@indigoai-us/hq-cli 5.345.151 → 5.345.153

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 (88) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/dist/bin/hq-auth-refresh.d.ts +4 -0
  3. package/dist/bin/hq-auth-refresh.js +9 -1
  4. package/dist/command-catalog.generated.d.ts +188 -3
  5. package/dist/command-catalog.generated.js +230 -3
  6. package/dist/command-registration-plan.d.ts +6 -0
  7. package/dist/command-registration-plan.js +1 -0
  8. package/dist/commands/agent-enroll.d.ts +12 -0
  9. package/dist/commands/agent-enroll.js +37 -4
  10. package/dist/commands/core-policy.js +4 -4
  11. package/dist/commands/core-session-project-native.js +19 -4
  12. package/dist/commands/decide.d.ts +13 -0
  13. package/dist/commands/decide.js +74 -0
  14. package/dist/commands/files.js +23 -7
  15. package/dist/commands/integrations-connect.js +24 -1
  16. package/dist/commands/integrations-core.d.ts +14 -0
  17. package/dist/commands/integrations-core.js +19 -1
  18. package/dist/commands/integrations-import.js +22 -1
  19. package/dist/commands/integrations.js +43 -2
  20. package/dist/commands/lanes-message.js +4 -11
  21. package/dist/commands/lanes-questions.js +3 -0
  22. package/dist/commands/lanes-resume.js +4 -11
  23. package/dist/commands/lanes-stories.js +3 -0
  24. package/dist/commands/rescue.d.ts +7 -0
  25. package/dist/commands/rescue.js +22 -13
  26. package/dist/commands/run.js +14 -5
  27. package/dist/commands/secrets.d.ts +13 -0
  28. package/dist/commands/secrets.js +51 -8
  29. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +186 -186
  30. package/dist/hq-cloud-sync-runner/package.json +1 -1
  31. package/dist/lib/agent-kit/adopt-identity.d.ts +7 -1
  32. package/dist/lib/agent-kit/adopt-identity.js +18 -3
  33. package/dist/lib/agent-kit/services.d.ts +3 -1
  34. package/dist/lib/agent-kit/services.js +5 -1
  35. package/dist/lib/daemon/ops/policy.d.ts +9 -1
  36. package/dist/lib/daemon/ops/policy.js +37 -2
  37. package/dist/lib/decisions/client.d.ts +67 -0
  38. package/dist/lib/decisions/client.js +120 -0
  39. package/dist/lib/decisions/expiry.d.ts +2 -0
  40. package/dist/lib/decisions/expiry.js +12 -0
  41. package/dist/lib/decisions/fixtures/us001.d.ts +137 -0
  42. package/dist/lib/decisions/fixtures/us001.js +21 -0
  43. package/dist/lib/decisions/import-lanes.d.ts +16 -0
  44. package/dist/lib/decisions/import-lanes.js +152 -0
  45. package/dist/lib/decisions/principal.d.ts +11 -0
  46. package/dist/lib/decisions/principal.js +52 -0
  47. package/dist/lib/doctor/checks/claude-wiring.d.ts +2 -2
  48. package/dist/lib/doctor/checks/claude-wiring.js +21 -10
  49. package/dist/lib/doctor/checks/work-context.js +7 -2
  50. package/dist/lib/doctor/fix/apply.d.ts +3 -2
  51. package/dist/lib/doctor/fix/apply.js +19 -15
  52. package/dist/lib/doctor/fix/refresh-sync-progress.d.ts +14 -0
  53. package/dist/lib/doctor/fix/refresh-sync-progress.js +30 -2
  54. package/dist/lib/doctor/fix/remediation.js +4 -2
  55. package/dist/lib/doctor/hook-gate-profiles.d.ts +19 -0
  56. package/dist/lib/doctor/hook-gate-profiles.js +34 -0
  57. package/dist/lib/doctor/types.d.ts +5 -0
  58. package/dist/lib/install/provider-wrappers.js +5 -1
  59. package/dist/lib/lanes/autopilot.js +4 -1
  60. package/dist/lib/lanes/imported-question.d.ts +5 -0
  61. package/dist/lib/lanes/imported-question.js +17 -0
  62. package/dist/lib/lanes/questions.d.ts +0 -5
  63. package/dist/lib/lanes/questions.js +2 -0
  64. package/dist/lib/lanes/reconcile.js +21 -10
  65. package/dist/lib/lanes/store.d.ts +0 -8
  66. package/dist/lib/lanes/store.js +2 -1
  67. package/dist/lib/lanes/stories.d.ts +15 -0
  68. package/dist/lib/lanes/stories.js +25 -0
  69. package/dist/lib/mesh/client.d.ts +8 -0
  70. package/dist/lib/mesh/client.js +44 -3
  71. package/dist/lib/mesh/denial-codes.js +1 -1
  72. package/dist/lib/mesh/live/daemon/run.d.ts +9 -1
  73. package/dist/lib/mesh/live/daemon/run.js +30 -1
  74. package/dist/lib/mesh/live/flush.d.ts +7 -0
  75. package/dist/lib/mesh/live/flush.js +53 -10
  76. package/dist/lib/registry/registry.js +6 -0
  77. package/dist/lib/scan-packages/index.js +7 -3
  78. package/dist/lib/work-context/organize.d.ts +2 -0
  79. package/dist/lib/work-context/organize.js +22 -4
  80. package/dist/lib/workers-registry/index.d.ts +12 -1
  81. package/dist/lib/workers-registry/index.js +39 -1
  82. package/dist/utils/cognito-session.d.ts +4 -1
  83. package/dist/utils/cognito-session.js +11 -1
  84. package/dist/utils/hook-trust.d.ts +27 -0
  85. package/dist/utils/hook-trust.js +150 -8
  86. package/dist/utils/shipped-codex-hooks.d.ts +17 -0
  87. package/dist/utils/shipped-codex-hooks.js +86 -0
  88. package/package.json +8 -8
@@ -1 +1 @@
1
- {"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.90","bundleSha256":"cb49486573d2d1141ee6e18d73dde825f2b7c02074560f19504e3b1daf7eeba8","formatVersion":1}
1
+ {"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.91","bundleSha256":"ea41f1765d5015a22304787abedc68a531393d601a349bb8bff3c5428413f3a3","formatVersion":1}
@@ -12,7 +12,11 @@
12
12
  *
13
13
  * So before any `hq agent` subcommand runs, the resolved tree is published to
14
14
  * the environment. An explicit `HQ_MACHINE_CREDS_FILE` is never overwritten —
15
- * a daemon unit that pins one stays pinned.
15
+ * a daemon unit that pins one stays pinned. A pinned creds file still gets its
16
+ * own token cache (beside the creds file) when `HQ_MACHINE_TOKEN_STATE_DIR` is
17
+ * unset, so an agent's minted tokens never land in the shared default cache
18
+ * at ~/.hq/work-mesh/daemon, where the deploy resolver on a person's laptop
19
+ * would read them as machine mode.
16
20
  *
17
21
  * The person's own session file is not read, moved, or changed by any of this.
18
22
  */
@@ -24,4 +28,6 @@ export declare const MACHINE_TOKEN_STATE_DIR_ENV = "HQ_MACHINE_TOKEN_STATE_DIR";
24
28
  * Returns the variables it set, for tests and for `--verbose` output.
25
29
  */
26
30
  export declare function adoptAgentIdentityEnv(paths: Pick<AgentKitPaths, "agentDir" | "machineCredsPath">, env?: NodeJS.ProcessEnv): Record<string, string>;
31
+ /** The agent's own minted-token cache directory, inside its tree. */
32
+ export declare function agentTokenStateDir(paths: Pick<AgentKitPaths, "agentDir">): string;
27
33
  //# sourceMappingURL=adopt-identity.d.ts.map
@@ -12,7 +12,11 @@
12
12
  *
13
13
  * So before any `hq agent` subcommand runs, the resolved tree is published to
14
14
  * the environment. An explicit `HQ_MACHINE_CREDS_FILE` is never overwritten —
15
- * a daemon unit that pins one stays pinned.
15
+ * a daemon unit that pins one stays pinned. A pinned creds file still gets its
16
+ * own token cache (beside the creds file) when `HQ_MACHINE_TOKEN_STATE_DIR` is
17
+ * unset, so an agent's minted tokens never land in the shared default cache
18
+ * at ~/.hq/work-mesh/daemon, where the deploy resolver on a person's laptop
19
+ * would read them as machine mode.
16
20
  *
17
21
  * The person's own session file is not read, moved, or changed by any of this.
18
22
  */
@@ -27,8 +31,15 @@ export const MACHINE_TOKEN_STATE_DIR_ENV = "HQ_MACHINE_TOKEN_STATE_DIR";
27
31
  */
28
32
  export function adoptAgentIdentityEnv(paths, env = process.env) {
29
33
  const applied = {};
30
- if (env[MACHINE_CREDS_FILE_ENV]?.trim())
34
+ const pinnedCreds = env[MACHINE_CREDS_FILE_ENV]?.trim();
35
+ if (pinnedCreds) {
36
+ if (!env[MACHINE_TOKEN_STATE_DIR_ENV]?.trim()) {
37
+ const dir = path.join(path.dirname(pinnedCreds), "token-state");
38
+ applied[MACHINE_TOKEN_STATE_DIR_ENV] = dir;
39
+ env[MACHINE_TOKEN_STATE_DIR_ENV] = dir;
40
+ }
31
41
  return applied;
42
+ }
32
43
  // Only adopt a tree that actually holds an identity: on a host that has not
33
44
  // enrolled yet, leaving the environment alone keeps today's error messages.
34
45
  try {
@@ -51,10 +62,14 @@ export function adoptAgentIdentityEnv(paths, env = process.env) {
51
62
  // Per-agent token cache. Two identities on one computer must never share a
52
63
  // minted-token file; the agent's belongs inside the agent's own tree.
53
64
  if (!env[MACHINE_TOKEN_STATE_DIR_ENV]?.trim()) {
54
- const dir = path.join(paths.agentDir, "token-state");
65
+ const dir = agentTokenStateDir(paths);
55
66
  applied[MACHINE_TOKEN_STATE_DIR_ENV] = dir;
56
67
  env[MACHINE_TOKEN_STATE_DIR_ENV] = dir;
57
68
  }
58
69
  return applied;
59
70
  }
71
+ /** The agent's own minted-token cache directory, inside its tree. */
72
+ export function agentTokenStateDir(paths) {
73
+ return path.join(paths.agentDir, "token-state");
74
+ }
60
75
  //# sourceMappingURL=adopt-identity.js.map
@@ -12,7 +12,9 @@ export declare function isKitService(value: string): value is KitService;
12
12
  * Environment every service inherits. HQ_MACHINE_CREDS_FILE pins hq-cloud's
13
13
  * mint to the kit's creds file; HQ_REQUIRE_MACHINE_IDENTITY makes a missing
14
14
  * file a loud error instead of a browser-login hang; HQ_AGENT_DIR keeps a
15
- * relocated kit consistent across restarts.
15
+ * relocated kit consistent across restarts. HQ_MACHINE_TOKEN_STATE_DIR keeps
16
+ * the agent's minted tokens in its own tree instead of the shared default
17
+ * cache a person's tools also read.
16
18
  */
17
19
  export declare function kitServiceEnv(paths: AgentKitPaths, env?: NodeJS.ProcessEnv): Record<string, string>;
18
20
  export declare function kitServiceSpec(service: KitService, paths: AgentKitPaths, env?: NodeJS.ProcessEnv): ServiceSpec;
@@ -4,6 +4,7 @@
4
4
  * machine-creds file itself, so the units carry paths and nothing else.
5
5
  */
6
6
  import { serviceLogPath } from "./paths.js";
7
+ import { agentTokenStateDir } from "./adopt-identity.js";
7
8
  export const KIT_SERVICES = ["sync", "inbox", "heartbeat"];
8
9
  const KIT_LABEL_PREFIX = "ai.getindigo.hq-agent";
9
10
  export function isKitService(value) {
@@ -18,13 +19,16 @@ const DESCRIPTIONS = {
18
19
  * Environment every service inherits. HQ_MACHINE_CREDS_FILE pins hq-cloud's
19
20
  * mint to the kit's creds file; HQ_REQUIRE_MACHINE_IDENTITY makes a missing
20
21
  * file a loud error instead of a browser-login hang; HQ_AGENT_DIR keeps a
21
- * relocated kit consistent across restarts.
22
+ * relocated kit consistent across restarts. HQ_MACHINE_TOKEN_STATE_DIR keeps
23
+ * the agent's minted tokens in its own tree instead of the shared default
24
+ * cache a person's tools also read.
22
25
  */
23
26
  export function kitServiceEnv(paths, env = process.env) {
24
27
  const flagsApiUrl = env.HQ_FLAGS_API_URL;
25
28
  return {
26
29
  HQ_AGENT_DIR: paths.agentDir,
27
30
  HQ_MACHINE_CREDS_FILE: paths.machineCredsPath,
31
+ HQ_MACHINE_TOKEN_STATE_DIR: agentTokenStateDir(paths),
28
32
  HQ_REQUIRE_MACHINE_IDENTITY: "1",
29
33
  ...(flagsApiUrl?.trim() ? { HQ_FLAGS_API_URL: flagsApiUrl } : {}),
30
34
  };
@@ -11,7 +11,11 @@
11
11
  *
12
12
  * The policy index (which policy files exist for a company) is cached
13
13
  * in-process and invalidated by fs.watch on core/policies, personal/policies
14
- * and companies/<co>/policies.
14
+ * and companies/<co>/policies. Each list() also compares the scanned
15
+ * directories' stat stamps with those taken during the last rebuild, because
16
+ * a watcher can miss a change: on macOS fs.watch is backed by an FSEvents
17
+ * stream that starts asynchronously, so a change made right after a rebuild
18
+ * re-arms the watchers is never reported.
15
19
  */
16
20
  export declare const SCOPE_AUTHORIZER = ".claude/hooks/mandatory-scope-authorizer.sh";
17
21
  export declare const POLICY_TRIGGER = ".claude/hooks/inject-policy-on-trigger.sh";
@@ -30,6 +34,8 @@ export declare class PolicyIndex {
30
34
  readonly hqRoot: string;
31
35
  private readonly log;
32
36
  private entries;
37
+ /** Directory stat stamps taken while building `entries` (dir -> stamp). */
38
+ private stamps;
33
39
  private watchers;
34
40
  private closed;
35
41
  /** Bumped on every invalidation; lets callers see that the cache was rebuilt. */
@@ -39,6 +45,8 @@ export declare class PolicyIndex {
39
45
  list(company?: string): PolicyEntry[];
40
46
  invalidate(): void;
41
47
  close(): void;
48
+ /** True when any directory scanned by the last rebuild changed since. */
49
+ private isStale;
42
50
  private rebuild;
43
51
  private rewatch;
44
52
  }
@@ -11,7 +11,11 @@
11
11
  *
12
12
  * The policy index (which policy files exist for a company) is cached
13
13
  * in-process and invalidated by fs.watch on core/policies, personal/policies
14
- * and companies/<co>/policies.
14
+ * and companies/<co>/policies. Each list() also compares the scanned
15
+ * directories' stat stamps with those taken during the last rebuild, because
16
+ * a watcher can miss a change: on macOS fs.watch is backed by an FSEvents
17
+ * stream that starts asynchronously, so a change made right after a rebuild
18
+ * re-arms the watchers is never reported.
15
19
  */
16
20
  import { spawn } from "node:child_process";
17
21
  import * as fs from "node:fs";
@@ -33,6 +37,8 @@ export class PolicyIndex {
33
37
  hqRoot;
34
38
  log;
35
39
  entries = null;
40
+ /** Directory stat stamps taken while building `entries` (dir -> stamp). */
41
+ stamps = new Map();
36
42
  watchers = [];
37
43
  closed = false;
38
44
  /** Bumped on every invalidation; lets callers see that the cache was rebuilt. */
@@ -43,6 +49,8 @@ export class PolicyIndex {
43
49
  }
44
50
  /** Policies visible to `company`: core, personal, and that company only. */
45
51
  list(company) {
52
+ if (this.entries && this.isStale())
53
+ this.invalidate();
46
54
  if (!this.entries)
47
55
  this.rebuild();
48
56
  return this.entries.filter((e) => e.scope !== "company" || e.company === company);
@@ -59,22 +67,38 @@ export class PolicyIndex {
59
67
  w.close();
60
68
  this.watchers = [];
61
69
  }
70
+ /** True when any directory scanned by the last rebuild changed since. */
71
+ isStale() {
72
+ for (const [dir, stamp] of this.stamps)
73
+ if (dirStamp(dir) !== stamp)
74
+ return true;
75
+ return false;
76
+ }
62
77
  rebuild() {
78
+ // Stamp each directory before reading it, so a change racing the read
79
+ // either shows up in the read or leaves a stale stamp for the next list().
80
+ const stamps = new Map();
63
81
  const dirs = [
64
82
  { dir: path.join(this.hqRoot, "core", "policies"), scope: "core" },
65
83
  { dir: path.join(this.hqRoot, "personal", "policies"), scope: "personal" },
66
84
  ];
67
85
  const companiesDir = path.join(this.hqRoot, "companies");
68
86
  const companyDirs = [];
87
+ for (const d of dirs)
88
+ stamps.set(d.dir, dirStamp(d.dir));
89
+ stamps.set(companiesDir, dirStamp(companiesDir));
69
90
  for (const co of readDirNames(companiesDir)) {
70
91
  if (!SLUG_RE.test(co))
71
92
  continue;
72
93
  const companyDir = path.join(companiesDir, co);
94
+ stamps.set(companyDir, dirStamp(companyDir));
73
95
  if (isDir(companyDir))
74
96
  companyDirs.push(companyDir);
75
97
  const dir = path.join(companiesDir, co, "policies");
76
- if (isDir(dir))
98
+ if (isDir(dir)) {
99
+ stamps.set(dir, dirStamp(dir));
77
100
  dirs.push({ dir, scope: "company", company: co });
101
+ }
78
102
  }
79
103
  const entries = [];
80
104
  for (const d of dirs) {
@@ -85,6 +109,7 @@ export class PolicyIndex {
85
109
  }
86
110
  }
87
111
  this.entries = entries;
112
+ this.stamps = stamps;
88
113
  this.rewatch([...dirs.map((d) => d.dir), ...companyDirs, companiesDir]);
89
114
  }
90
115
  rewatch(dirs) {
@@ -120,6 +145,16 @@ function readDirNames(dir) {
120
145
  throw err;
121
146
  }
122
147
  }
148
+ /** Identity plus modification time of a directory, or "missing". */
149
+ function dirStamp(dir) {
150
+ try {
151
+ const st = fs.statSync(dir, { bigint: true });
152
+ return `${st.dev}:${st.ino}:${st.mtimeNs}`;
153
+ }
154
+ catch {
155
+ return "missing";
156
+ }
157
+ }
123
158
  function isDir(dir) {
124
159
  try {
125
160
  return fs.statSync(dir).isDirectory();
@@ -0,0 +1,67 @@
1
+ import { vaultApiFetch } from "../../utils/vault-api.js";
2
+ export type DecisionState = "open" | "talking" | "answered" | "expired" | "withdrawn";
3
+ export interface DecisionOption {
4
+ id: string;
5
+ label: string;
6
+ recommended: boolean;
7
+ }
8
+ export interface Decision {
9
+ id: string;
10
+ askerPrincipal: string;
11
+ companyUid: string;
12
+ audience: {
13
+ kind: "person";
14
+ personUid: string;
15
+ } | {
16
+ kind: "company_admins";
17
+ };
18
+ statement: string;
19
+ options: DecisionOption[];
20
+ context: {
21
+ links: string[];
22
+ brief: string;
23
+ commitmentId: string | null;
24
+ };
25
+ state: DecisionState;
26
+ answer: {
27
+ choiceId: string;
28
+ by: string;
29
+ at: string;
30
+ source: "tap" | "thread" | "desktop";
31
+ } | null;
32
+ talkThreadId: string | null;
33
+ createdAt: string;
34
+ expiresAt: string;
35
+ updatedAt: string;
36
+ }
37
+ export interface AskDecision {
38
+ companyUid: string;
39
+ expiresAt: string;
40
+ audience: {
41
+ kind: "person";
42
+ personUid: string;
43
+ };
44
+ statement: string;
45
+ options: DecisionOption[];
46
+ context: Decision["context"];
47
+ }
48
+ export declare class DecisionError extends Error {
49
+ readonly code: string;
50
+ readonly status?: number | undefined;
51
+ constructor(code: string, message: string, status?: number | undefined);
52
+ }
53
+ /** US-001 transport envelopes live here so real producer fixtures replace one boundary. */
54
+ export declare function parseDecision(raw: unknown): Decision;
55
+ export declare function parseOptions(raw: string[], recommend?: string): DecisionOption[];
56
+ export declare class DecisionsClient {
57
+ private readonly token;
58
+ private readonly request;
59
+ constructor(token: () => Promise<string>, request?: typeof vaultApiFetch);
60
+ private call;
61
+ ask(input: AskDecision): Promise<Decision>;
62
+ list(open?: boolean): Promise<Decision[]>;
63
+ show(id: string): Promise<Decision>;
64
+ answer(id: string, choiceId: string, source: "desktop" | "thread"): Promise<Decision>;
65
+ transition(id: string, action: "answer" | "talk" | "withdraw", body?: Record<string, unknown>): Promise<Decision>;
66
+ }
67
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1,120 @@
1
+ import { vaultApiFetch } from "../../utils/vault-api.js";
2
+ import { assertNoSecretText } from "../lanes/text-scan.js";
3
+ export class DecisionError extends Error {
4
+ code;
5
+ status;
6
+ constructor(code, message, status) {
7
+ super(`${code}: ${message}`);
8
+ this.code = code;
9
+ this.status = status;
10
+ }
11
+ }
12
+ function object(value) {
13
+ if (!value || typeof value !== "object" || Array.isArray(value))
14
+ throw new DecisionError("invalid_decision_response", "Expected an object");
15
+ return value;
16
+ }
17
+ function string(value) {
18
+ if (typeof value !== "string" || !value.trim())
19
+ throw new DecisionError("invalid_decision_response", "Expected a non-empty string");
20
+ return value;
21
+ }
22
+ /** US-001 transport envelopes live here so real producer fixtures replace one boundary. */
23
+ export function parseDecision(raw) {
24
+ const row = object(raw);
25
+ const context = object(row.context);
26
+ const audience = object(row.audience);
27
+ if (audience.kind !== "person" && audience.kind !== "company_admins")
28
+ throw new DecisionError("invalid_decision_response", "Unknown audience kind");
29
+ if (!Array.isArray(row.options) || !Array.isArray(context.links))
30
+ throw new DecisionError("invalid_decision_response", "Expected options and context.links arrays");
31
+ const state = string(row.state);
32
+ if (!["open", "talking", "answered", "expired", "withdrawn"].includes(state))
33
+ throw new DecisionError("invalid_decision_response", "Unknown state");
34
+ const options = row.options.map(rawOption => {
35
+ const option = object(rawOption);
36
+ if (typeof option.recommended !== "boolean")
37
+ throw new DecisionError("invalid_decision_response", "Expected recommended boolean");
38
+ return { id: string(option.id), label: string(option.label), recommended: option.recommended };
39
+ });
40
+ if (new Set(options.map(option => option.id)).size !== options.length)
41
+ throw new DecisionError("invalid_decision_response", "Duplicate option ids");
42
+ if (typeof context.brief !== "string")
43
+ throw new DecisionError("invalid_decision_response", "Expected context brief");
44
+ if (context.commitmentId !== null && typeof context.commitmentId !== "string")
45
+ throw new DecisionError("invalid_decision_response", "Expected nullable commitment id");
46
+ const answer = row.answer == null ? row.answer : object(row.answer);
47
+ if (answer && !["tap", "thread", "desktop"].includes(string(answer.source)))
48
+ throw new DecisionError("invalid_decision_response", "Unknown answer source");
49
+ return {
50
+ id: string(row.id), askerPrincipal: string(row.askerPrincipal), companyUid: string(row.companyUid),
51
+ audience: audience.kind === "person" ? { kind: "person", personUid: string(audience.personUid) } : { kind: "company_admins" },
52
+ statement: string(row.statement), options, state,
53
+ context: { links: context.links.map(string), brief: context.brief, commitmentId: context.commitmentId },
54
+ ...(answer == null ? { answer: null } : { answer: { choiceId: string(answer.choiceId), by: string(answer.by), at: string(answer.at), source: answer.source } }),
55
+ ...(row.talkThreadId == null ? { talkThreadId: null } : { talkThreadId: string(row.talkThreadId) }),
56
+ createdAt: string(row.createdAt), updatedAt: string(row.updatedAt),
57
+ expiresAt: string(row.expiresAt),
58
+ };
59
+ }
60
+ export function parseOptions(raw, recommend) {
61
+ if (raw.length < 2)
62
+ throw new DecisionError("invalid_options", "Supply at least two --option 'id|label' choices");
63
+ const options = raw.map(value => {
64
+ const index = value.indexOf("|");
65
+ const id = value.slice(0, index).trim();
66
+ const label = value.slice(index + 1).trim();
67
+ if (index < 1 || !/^[A-Za-z0-9_-]+$/.test(id) || !label)
68
+ throw new DecisionError("invalid_options", "Each option must be id|label");
69
+ return { id, label, recommended: id === recommend };
70
+ });
71
+ if (new Set(options.map(option => option.id)).size !== options.length || (recommend !== undefined && !options.some(option => option.id === recommend))) {
72
+ throw new DecisionError("invalid_options", "Option ids must be unique and --recommend must name one");
73
+ }
74
+ return options;
75
+ }
76
+ export class DecisionsClient {
77
+ token;
78
+ request;
79
+ constructor(token, request = vaultApiFetch) {
80
+ this.token = token;
81
+ this.request = request;
82
+ }
83
+ async call(path, method = "GET", body, query) {
84
+ const response = await this.request({ token: await this.token(), path, method, body, query, signal: AbortSignal.timeout(30_000) });
85
+ if (!response.ok) {
86
+ // Do not echo arbitrary server bodies (which may contain confidential input).
87
+ const codes = new Set(["invalid_decision", "invalid_answer", "invalid_expiry", "invalid_audience", "invalid_state", "invalid_json", "invalid_audience_filter", "asker_not_allowed", "company_forbidden", "agent_owner_only", "agent_owner_required", "decision_not_found", "already_answered", "decision_expired", "decision_withdrawn", "decision_closed", "decision_conflict", "route_not_found"]);
88
+ const payload = await response.json().catch(() => null);
89
+ const code = typeof payload?.code === "string" && codes.has(payload.code) ? payload.code : "decision_request_failed";
90
+ throw new DecisionError(code, `${method} ${path} returned ${response.status}`, response.status);
91
+ }
92
+ return object(await response.json());
93
+ }
94
+ async ask(input) {
95
+ assertNoSecretText("decision", JSON.stringify(input));
96
+ return parseDecision((await this.call("/v1/decisions", "POST", { ...input })).decision);
97
+ }
98
+ async list(open = false) {
99
+ const result = await this.call("/v1/decisions", "GET", undefined, { for: "me", ...(open ? { state: "open" } : {}) });
100
+ if (!Array.isArray(result.decisions))
101
+ throw new DecisionError("invalid_decision_response", "Expected decisions array");
102
+ return result.decisions.map(parseDecision).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
103
+ }
104
+ async show(id) {
105
+ // US-001 has no GET-by-id route. Use the authorised across-company list.
106
+ const record = (await this.list()).find(decision => decision.id === id);
107
+ if (!record)
108
+ throw new DecisionError("decision_not_found", `Decision ${id} is not visible to this principal`);
109
+ return record;
110
+ }
111
+ async answer(id, choiceId, source) {
112
+ return this.transition(id, "answer", { choiceId, source });
113
+ }
114
+ async transition(id, action, body = {}) {
115
+ if (!id.trim())
116
+ throw new DecisionError("invalid_decision_id", "Decision id is required");
117
+ return parseDecision((await this.call(`/v1/decisions/${encodeURIComponent(id)}/${action}`, "POST", body)).decision);
118
+ }
119
+ }
120
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,2 @@
1
+ export declare function decisionExpiry(value?: string, now?: number): string;
2
+ //# sourceMappingURL=expiry.d.ts.map
@@ -0,0 +1,12 @@
1
+ import { DecisionError } from "./client.js";
2
+ export function decisionExpiry(value = "72h", now = Date.now()) {
3
+ const duration = /^(\d+(?:\.\d+)?)(m|h|d)$/.exec(value);
4
+ const timestamp = duration
5
+ ? now + Number(duration[1]) * ({ m: 60_000, h: 3_600_000, d: 86_400_000 }[duration[2]] ?? 0)
6
+ : /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(value) ? Date.parse(value) : NaN;
7
+ if (!Number.isFinite(timestamp) || timestamp <= now || timestamp > 8.64e15) {
8
+ throw new DecisionError("invalid_expiry", "Use a future ISO date-time or positive duration such as 72h");
9
+ }
10
+ return new Date(timestamp).toISOString();
11
+ }
12
+ //# sourceMappingURL=expiry.js.map
@@ -0,0 +1,137 @@
1
+ /** Producer-service fixtures verified against hq-pro PR #5060 at 7c234efa (in-memory service output; handler wrappers). */
2
+ import type { Decision } from "../client.js";
3
+ export declare const openDecision: Decision;
4
+ export declare const decisionFixtures: {
5
+ create: {
6
+ decision: Decision;
7
+ };
8
+ list: {
9
+ decisions: Decision[];
10
+ };
11
+ answer: {
12
+ decision: {
13
+ state: string;
14
+ updatedAt: string;
15
+ answer: {
16
+ choiceId: string;
17
+ by: string;
18
+ at: string;
19
+ source: string;
20
+ };
21
+ id: string;
22
+ askerPrincipal: string;
23
+ companyUid: string;
24
+ audience: {
25
+ kind: "person";
26
+ personUid: string;
27
+ } | {
28
+ kind: "company_admins";
29
+ };
30
+ statement: string;
31
+ options: import("../client.js").DecisionOption[];
32
+ context: {
33
+ links: string[];
34
+ brief: string;
35
+ commitmentId: string | null;
36
+ };
37
+ talkThreadId: string | null;
38
+ createdAt: string;
39
+ expiresAt: string;
40
+ };
41
+ };
42
+ talk: {
43
+ decision: {
44
+ state: string;
45
+ updatedAt: string;
46
+ talkThreadId: null;
47
+ id: string;
48
+ askerPrincipal: string;
49
+ companyUid: string;
50
+ audience: {
51
+ kind: "person";
52
+ personUid: string;
53
+ } | {
54
+ kind: "company_admins";
55
+ };
56
+ statement: string;
57
+ options: import("../client.js").DecisionOption[];
58
+ context: {
59
+ links: string[];
60
+ brief: string;
61
+ commitmentId: string | null;
62
+ };
63
+ answer: {
64
+ choiceId: string;
65
+ by: string;
66
+ at: string;
67
+ source: "tap" | "thread" | "desktop";
68
+ } | null;
69
+ createdAt: string;
70
+ expiresAt: string;
71
+ };
72
+ };
73
+ withdraw: {
74
+ decision: {
75
+ state: string;
76
+ updatedAt: string;
77
+ id: string;
78
+ askerPrincipal: string;
79
+ companyUid: string;
80
+ audience: {
81
+ kind: "person";
82
+ personUid: string;
83
+ } | {
84
+ kind: "company_admins";
85
+ };
86
+ statement: string;
87
+ options: import("../client.js").DecisionOption[];
88
+ context: {
89
+ links: string[];
90
+ brief: string;
91
+ commitmentId: string | null;
92
+ };
93
+ answer: {
94
+ choiceId: string;
95
+ by: string;
96
+ at: string;
97
+ source: "tap" | "thread" | "desktop";
98
+ } | null;
99
+ talkThreadId: string | null;
100
+ createdAt: string;
101
+ expiresAt: string;
102
+ };
103
+ };
104
+ expired: {
105
+ decision: {
106
+ state: string;
107
+ updatedAt: string;
108
+ id: string;
109
+ askerPrincipal: string;
110
+ companyUid: string;
111
+ audience: {
112
+ kind: "person";
113
+ personUid: string;
114
+ } | {
115
+ kind: "company_admins";
116
+ };
117
+ statement: string;
118
+ options: import("../client.js").DecisionOption[];
119
+ context: {
120
+ links: string[];
121
+ brief: string;
122
+ commitmentId: string | null;
123
+ };
124
+ answer: {
125
+ choiceId: string;
126
+ by: string;
127
+ at: string;
128
+ source: "tap" | "thread" | "desktop";
129
+ } | null;
130
+ talkThreadId: string | null;
131
+ createdAt: string;
132
+ expiresAt: string;
133
+ };
134
+ };
135
+ };
136
+ export declare const decisionRoutes: string[][];
137
+ //# sourceMappingURL=us001.d.ts.map
@@ -0,0 +1,21 @@
1
+ export const openDecision = {
2
+ id: "f3ccdf93-9b7e-4564-a3ad-566067900aee", companyUid: "cmp_indigo", askerPrincipal: "agt_personal", audience: { kind: "person", personUid: "prs_owner" },
3
+ statement: "Release the update?",
4
+ options: [{ id: "A", label: "Release", recommended: true }, { id: "B", label: "Wait", recommended: false }],
5
+ context: { links: ["https://example.com/review"], brief: "Checks passed", commitmentId: "cm_1" },
6
+ state: "open", answer: null, talkThreadId: null,
7
+ createdAt: "2026-10-18T02:00:00.000Z", expiresAt: "2026-10-19T02:00:00.000Z", updatedAt: "2026-10-18T02:00:00.000Z",
8
+ };
9
+ export const decisionFixtures = {
10
+ create: { decision: openDecision },
11
+ list: { decisions: [openDecision] },
12
+ answer: { decision: { ...openDecision, state: "answered", updatedAt: "2026-10-18T03:00:00.000Z", answer: { choiceId: "A", by: "prs_owner", at: "2026-10-18T03:00:00.000Z", source: "desktop" } } },
13
+ talk: { decision: { ...openDecision, state: "talking", updatedAt: "2026-10-18T03:00:00.000Z", talkThreadId: null } },
14
+ withdraw: { decision: { ...openDecision, state: "withdrawn", updatedAt: "2026-10-18T03:00:00.000Z" } },
15
+ expired: { decision: { ...openDecision, state: "expired", updatedAt: "2026-10-19T03:00:00.000Z" } },
16
+ };
17
+ export const decisionRoutes = [
18
+ ["POST", "/v1/decisions"], ["GET", "/v1/decisions"],
19
+ ["POST", "/v1/decisions/{id}/answer"], ["POST", "/v1/decisions/{id}/talk"], ["POST", "/v1/decisions/{id}/withdraw"],
20
+ ];
21
+ //# sourceMappingURL=us001.js.map
@@ -0,0 +1,16 @@
1
+ import { type QuestionBody } from "../lanes/questions.js";
2
+ import type { QuestionRecord } from "../lanes/types.js";
3
+ import { DecisionsClient, type AskDecision } from "./client.js";
4
+ export declare function legacyDecisionInput(record: QuestionRecord, body: QuestionBody, audience: string): AskDecision | undefined;
5
+ export interface ImportCounts {
6
+ imported: number;
7
+ alreadyImported: number;
8
+ skipped: number;
9
+ skippedQuestions: Array<{
10
+ id: string;
11
+ reason: string;
12
+ }>;
13
+ }
14
+ /** Read legacy inputs only. Receipts live separately; legacy lane commands keep their storage. */
15
+ export declare function importLaneQuestions(root: string, audience: string, client: Pick<DecisionsClient, "ask" | "list">): Promise<ImportCounts>;
16
+ //# sourceMappingURL=import-lanes.d.ts.map