@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.
- package/CHANGELOG.md +129 -0
- package/dist/bin/hq-auth-refresh.d.ts +4 -0
- package/dist/bin/hq-auth-refresh.js +9 -1
- package/dist/command-catalog.generated.d.ts +188 -3
- package/dist/command-catalog.generated.js +230 -3
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-enroll.d.ts +12 -0
- package/dist/commands/agent-enroll.js +37 -4
- package/dist/commands/core-policy.js +4 -4
- package/dist/commands/core-session-project-native.js +19 -4
- package/dist/commands/decide.d.ts +13 -0
- package/dist/commands/decide.js +74 -0
- package/dist/commands/files.js +23 -7
- package/dist/commands/integrations-connect.js +24 -1
- package/dist/commands/integrations-core.d.ts +14 -0
- package/dist/commands/integrations-core.js +19 -1
- package/dist/commands/integrations-import.js +22 -1
- package/dist/commands/integrations.js +43 -2
- package/dist/commands/lanes-message.js +4 -11
- package/dist/commands/lanes-questions.js +3 -0
- package/dist/commands/lanes-resume.js +4 -11
- package/dist/commands/lanes-stories.js +3 -0
- package/dist/commands/rescue.d.ts +7 -0
- package/dist/commands/rescue.js +22 -13
- package/dist/commands/run.js +14 -5
- package/dist/commands/secrets.d.ts +13 -0
- package/dist/commands/secrets.js +51 -8
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +186 -186
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/adopt-identity.d.ts +7 -1
- package/dist/lib/agent-kit/adopt-identity.js +18 -3
- package/dist/lib/agent-kit/services.d.ts +3 -1
- package/dist/lib/agent-kit/services.js +5 -1
- package/dist/lib/daemon/ops/policy.d.ts +9 -1
- package/dist/lib/daemon/ops/policy.js +37 -2
- package/dist/lib/decisions/client.d.ts +67 -0
- package/dist/lib/decisions/client.js +120 -0
- package/dist/lib/decisions/expiry.d.ts +2 -0
- package/dist/lib/decisions/expiry.js +12 -0
- package/dist/lib/decisions/fixtures/us001.d.ts +137 -0
- package/dist/lib/decisions/fixtures/us001.js +21 -0
- package/dist/lib/decisions/import-lanes.d.ts +16 -0
- package/dist/lib/decisions/import-lanes.js +152 -0
- package/dist/lib/decisions/principal.d.ts +11 -0
- package/dist/lib/decisions/principal.js +52 -0
- package/dist/lib/doctor/checks/claude-wiring.d.ts +2 -2
- package/dist/lib/doctor/checks/claude-wiring.js +21 -10
- package/dist/lib/doctor/checks/work-context.js +7 -2
- package/dist/lib/doctor/fix/apply.d.ts +3 -2
- package/dist/lib/doctor/fix/apply.js +19 -15
- package/dist/lib/doctor/fix/refresh-sync-progress.d.ts +14 -0
- package/dist/lib/doctor/fix/refresh-sync-progress.js +30 -2
- package/dist/lib/doctor/fix/remediation.js +4 -2
- package/dist/lib/doctor/hook-gate-profiles.d.ts +19 -0
- package/dist/lib/doctor/hook-gate-profiles.js +34 -0
- package/dist/lib/doctor/types.d.ts +5 -0
- package/dist/lib/install/provider-wrappers.js +5 -1
- package/dist/lib/lanes/autopilot.js +4 -1
- package/dist/lib/lanes/imported-question.d.ts +5 -0
- package/dist/lib/lanes/imported-question.js +17 -0
- package/dist/lib/lanes/questions.d.ts +0 -5
- package/dist/lib/lanes/questions.js +2 -0
- package/dist/lib/lanes/reconcile.js +21 -10
- package/dist/lib/lanes/store.d.ts +0 -8
- package/dist/lib/lanes/store.js +2 -1
- package/dist/lib/lanes/stories.d.ts +15 -0
- package/dist/lib/lanes/stories.js +25 -0
- package/dist/lib/mesh/client.d.ts +8 -0
- package/dist/lib/mesh/client.js +44 -3
- package/dist/lib/mesh/denial-codes.js +1 -1
- package/dist/lib/mesh/live/daemon/run.d.ts +9 -1
- package/dist/lib/mesh/live/daemon/run.js +30 -1
- package/dist/lib/mesh/live/flush.d.ts +7 -0
- package/dist/lib/mesh/live/flush.js +53 -10
- package/dist/lib/registry/registry.js +6 -0
- package/dist/lib/scan-packages/index.js +7 -3
- package/dist/lib/work-context/organize.d.ts +2 -0
- package/dist/lib/work-context/organize.js +22 -4
- package/dist/lib/workers-registry/index.d.ts +12 -1
- package/dist/lib/workers-registry/index.js +39 -1
- package/dist/utils/cognito-session.d.ts +4 -1
- package/dist/utils/cognito-session.js +11 -1
- package/dist/utils/hook-trust.d.ts +27 -0
- package/dist/utils/hook-trust.js +150 -8
- package/dist/utils/shipped-codex-hooks.d.ts +17 -0
- package/dist/utils/shipped-codex-hooks.js +86 -0
- package/package.json +8 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.
|
|
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
|
-
|
|
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 =
|
|
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,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
|