@indigoai-us/hq-cli 5.345.47 → 5.345.49
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 +15 -0
- package/README.md +2 -1
- package/assets/scaffold/core/scripts/hq-session.sh +113 -7
- package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +101 -0
- package/dist/command-catalog.generated.d.ts +12 -0
- package/dist/command-catalog.generated.js +15 -0
- package/dist/commands/anywhere-runtime-flag.js +5 -3
- package/dist/commands/core-hq-session-native.d.ts +2 -0
- package/dist/commands/core-hq-session-native.js +242 -52
- package/dist/commands/core-resolve-company-native.d.ts +1 -0
- package/dist/commands/core-resolve-company-native.js +41 -11
- package/dist/commands/feedback.d.ts +10 -0
- package/dist/commands/feedback.js +45 -0
- package/dist/commands/lanes-message.js +2 -2
- package/dist/commands/lanes.js +2 -2
- package/dist/commands/mcp-serve.js +17 -4
- package/dist/commands/secrets.d.ts +5 -0
- package/dist/commands/secrets.js +181 -2
- package/dist/commands/workers.d.ts +4 -1
- package/dist/commands/workers.js +57 -15
- package/dist/lib/anywhere-person-setting.d.ts +1 -0
- package/dist/lib/anywhere-person-setting.js +12 -0
- package/dist/lib/anywhere-runtime-fast-flag.js +5 -3
- package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -1
- package/dist/lib/anywhere-runtime-flag-key.js +6 -1
- package/dist/lib/cli-flag-keys.d.ts +1 -0
- package/dist/lib/cli-flag-keys.js +1 -0
- package/dist/lib/core-utils/session-id.d.ts +6 -0
- package/dist/lib/core-utils/session-id.js +35 -0
- package/dist/lib/daemon/catalog.js +1 -1
- package/dist/lib/daemon/flag.d.ts +1 -1
- package/dist/lib/daemon/flag.js +20 -9
- package/dist/lib/daemon/lanes-reconcile-guard.d.ts +6 -5
- package/dist/lib/daemon/lanes-reconcile-guard.js +26 -7
- package/dist/lib/daemon/run.d.ts +6 -0
- package/dist/lib/daemon/run.js +36 -9
- package/dist/lib/daemon/server.d.ts +1 -1
- package/dist/lib/daemon/supervisor.d.ts +7 -1
- package/dist/lib/daemon/supervisor.js +50 -4
- package/dist/lib/lanes/reconcile.js +6 -4
- package/dist/lib/lanes/registry-flag.d.ts +11 -1
- package/dist/lib/lanes/registry-flag.js +68 -29
- package/dist/lib/lanes/watcher.js +63 -2
- package/dist/utils/resident-package-preload.js +8 -1
- package/dist/utils/self-update.js +6 -5
- package/dist/utils/version-gate.d.ts +11 -4
- package/dist/utils/version-gate.js +132 -41
- package/docs/anywhere-runtime.md +5 -3
- package/package.json +1 -1
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
5
6
|
import { spawn, spawnSync } from "node:child_process";
|
|
6
7
|
import { awkCannotOpenFileDiagnostic } from "../lib/core-utils/awk-diagnostics.js";
|
|
7
8
|
import { shellDisplayPath } from "../lib/core-utils/shell-display-path.js";
|
|
9
|
+
import { validSessionId, resolveSessionId as resolveSharedSessionId } from "../lib/core-utils/session-id.js";
|
|
8
10
|
export const HQ_SESSION_ASSET = "core/scripts/hq-session.sh";
|
|
11
|
+
const requireSessionDependency = createRequire(import.meta.url);
|
|
9
12
|
export const HQ_SESSION_HELP = [
|
|
10
13
|
"hq-core: public",
|
|
11
14
|
"hq-session.sh — read/write the current session's metadata.",
|
|
@@ -15,6 +18,9 @@ export const HQ_SESSION_HELP = [
|
|
|
15
18
|
" core/scripts/hq-session.sh path # print path to current meta.yaml",
|
|
16
19
|
" core/scripts/hq-session.sh get <key> # read a key from meta.yaml",
|
|
17
20
|
" core/scripts/hq-session.sh set <key> <value> # set/replace a top-level key",
|
|
21
|
+
" add company <slug> # add a company to the session lock set",
|
|
22
|
+
" remove company <slug> # remove a company from the session lock set",
|
|
23
|
+
" get company_slugs # print locked companies, primary first",
|
|
18
24
|
"",
|
|
19
25
|
"Reserved value:",
|
|
20
26
|
" set company_slug personal # bind the session to no-company (personal) work",
|
|
@@ -105,44 +111,69 @@ const defaultDependencies = () => ({
|
|
|
105
111
|
...(result.error ? { error: result.error } : {}),
|
|
106
112
|
};
|
|
107
113
|
},
|
|
114
|
+
multiCompanyLockEnabled: false,
|
|
108
115
|
});
|
|
109
116
|
function isExpectedFsMiss(error) {
|
|
110
117
|
const code = error?.code;
|
|
111
118
|
return code === "ENOENT" || code === "ENOTDIR" || code === "EACCES" || code === "ELOOP";
|
|
112
119
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
function validSessionId(value) {
|
|
117
|
-
return value.length > 0 && value !== "." && value !== ".." && /^[A-Za-z0-9._-]+$/.test(value);
|
|
118
|
-
}
|
|
119
|
-
function sessionIdFromEnv(env) {
|
|
120
|
-
for (const name of [
|
|
121
|
-
"HQ_SESSION_ID", "CLAUDE_CODE_SESSION_ID", "CLAUDE_SESSION_ID",
|
|
122
|
-
"CODEX_SESSION_ID", "CODEX_THREAD_ID", "GROK_SESSION_ID",
|
|
123
|
-
]) {
|
|
124
|
-
const value = asciiWhitespace(env[name] ?? "");
|
|
125
|
-
if (validSessionId(value))
|
|
126
|
-
return value;
|
|
127
|
-
}
|
|
128
|
-
return "";
|
|
120
|
+
export { validSessionId } from "../lib/core-utils/session-id.js";
|
|
121
|
+
function resolveSessionId(root, deps) {
|
|
122
|
+
return resolveSharedSessionId(root, deps.env, deps.readFile);
|
|
129
123
|
}
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return "";
|
|
124
|
+
function isRegisteredCompany(root, slug, deps) {
|
|
125
|
+
if (!slug || slug.startsWith("_") || slug === "companies" || slug === "unaffiliated_repos")
|
|
126
|
+
return false;
|
|
134
127
|
try {
|
|
135
|
-
const
|
|
136
|
-
|
|
128
|
+
const yaml = requireSessionDependency("js-yaml");
|
|
129
|
+
const value = yaml.load(deps.readFile(path.join(root, "companies", "manifest.yaml"), "utf8"));
|
|
130
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
131
|
+
return false;
|
|
132
|
+
const companies = value.companies;
|
|
133
|
+
return Boolean(companies && typeof companies === "object" && !Array.isArray(companies) && Object.hasOwn(companies, slug));
|
|
137
134
|
}
|
|
138
|
-
catch
|
|
139
|
-
|
|
140
|
-
return "";
|
|
141
|
-
throw error;
|
|
135
|
+
catch {
|
|
136
|
+
return false;
|
|
142
137
|
}
|
|
143
138
|
}
|
|
144
|
-
function
|
|
145
|
-
|
|
139
|
+
function withCompanyLock(root, sessionId, action) {
|
|
140
|
+
const lock = path.join(root, "workspace", "sessions", sessionId, ".company-lock-set.lock");
|
|
141
|
+
const deadline = Date.now() + 30_000;
|
|
142
|
+
while (true) {
|
|
143
|
+
try {
|
|
144
|
+
fs.mkdirSync(lock);
|
|
145
|
+
fs.writeFileSync(path.join(lock, "pid"), `${process.pid}\n`, "utf8");
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error.code !== "EEXIST")
|
|
150
|
+
throw error;
|
|
151
|
+
try {
|
|
152
|
+
const owner = Number.parseInt(fs.readFileSync(path.join(lock, "pid"), "utf8").trim(), 10);
|
|
153
|
+
if (Number.isInteger(owner) && owner > 0) {
|
|
154
|
+
try {
|
|
155
|
+
process.kill(owner, 0);
|
|
156
|
+
}
|
|
157
|
+
catch (probeError) {
|
|
158
|
+
if (probeError.code === "ESRCH") {
|
|
159
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch { /* The shell writer may be between mkdir and writing pid. */ }
|
|
166
|
+
if (Date.now() >= deadline)
|
|
167
|
+
throw new Error("hq-session: timed out waiting for company lock update");
|
|
168
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
return action();
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
176
|
+
}
|
|
146
177
|
}
|
|
147
178
|
function findMetaValue(contents, key) {
|
|
148
179
|
for (const line of contents.split("\n")) {
|
|
@@ -155,6 +186,50 @@ function findMetaValue(contents, key) {
|
|
|
155
186
|
return { found: false, value: "" };
|
|
156
187
|
}
|
|
157
188
|
function getMetaValue(contents, key) { return findMetaValue(contents, key).value; }
|
|
189
|
+
function writeCompanyMetadata(contents, companies) {
|
|
190
|
+
const primary = companies[0] ?? "";
|
|
191
|
+
const records = contents.split("\n");
|
|
192
|
+
if (records.at(-1) === "")
|
|
193
|
+
records.pop();
|
|
194
|
+
const replaced = [];
|
|
195
|
+
let havePrimary = false;
|
|
196
|
+
let haveCompanies = false;
|
|
197
|
+
for (const line of records) {
|
|
198
|
+
const first = line.match(/^\s*(\S+)/)?.[1] ?? "";
|
|
199
|
+
if (first === "company_slug:") {
|
|
200
|
+
if (!havePrimary)
|
|
201
|
+
replaced.push(`company_slug: ${primary}`);
|
|
202
|
+
havePrimary = true;
|
|
203
|
+
}
|
|
204
|
+
else if (first === "company_slugs:") {
|
|
205
|
+
if (!haveCompanies)
|
|
206
|
+
replaced.push(`company_slugs: ${companies.join(",")}`);
|
|
207
|
+
haveCompanies = true;
|
|
208
|
+
}
|
|
209
|
+
else
|
|
210
|
+
replaced.push(line);
|
|
211
|
+
}
|
|
212
|
+
if (!havePrimary)
|
|
213
|
+
replaced.push(`company_slug: ${primary}`);
|
|
214
|
+
if (!haveCompanies)
|
|
215
|
+
replaced.push(`company_slugs: ${companies.join(",")}`);
|
|
216
|
+
return `${replaced.join("\n")}\n`;
|
|
217
|
+
}
|
|
218
|
+
function metaCompanySlugs(contents) {
|
|
219
|
+
const listed = getMetaValue(contents, "company_slugs");
|
|
220
|
+
const primary = getMetaValue(contents, "company_slug");
|
|
221
|
+
const candidates = listed ? listed.split(",") : primary ? [primary] : [];
|
|
222
|
+
const seen = new Set();
|
|
223
|
+
const result = candidates.filter((slug) => {
|
|
224
|
+
if (!/^[A-Za-z0-9_-]+$/.test(slug) || seen.has(slug))
|
|
225
|
+
return false;
|
|
226
|
+
seen.add(slug);
|
|
227
|
+
return true;
|
|
228
|
+
});
|
|
229
|
+
if (result.length > 1 && result.includes("personal"))
|
|
230
|
+
return primary ? [primary] : [];
|
|
231
|
+
return primary && result[0] !== primary ? [primary] : result;
|
|
232
|
+
}
|
|
158
233
|
const WORK_MESH_SESSION_START_COMMAND = 'event=$HQ_CORE_SESSION_EVENT_JSON; unset HQ_CORE_SESSION_EVENT_JSON; printf "%s" "$event" | bash "$1" SessionStart';
|
|
159
234
|
function recordWorkMeshSessionStart(input) {
|
|
160
235
|
const hook = path.join(input.root, "core", "hooks", "SessionStart", "35-work-mesh-session-start.sh");
|
|
@@ -218,8 +293,33 @@ function runWorkMeshProjectRebind(input) {
|
|
|
218
293
|
}
|
|
219
294
|
return { stdout: result.stdout, stderr: result.stderr };
|
|
220
295
|
}
|
|
296
|
+
function registerWorkMeshCompany(root, sessionId, company, metadata, cwd, deps) {
|
|
297
|
+
const library = path.join(root, "core", "scripts", "lib");
|
|
298
|
+
if (!deps.isFile(path.join(library, "work-mesh-enqueue.sh")) ||
|
|
299
|
+
!deps.isFile(path.join(library, "work-mesh-live-rebind.sh")))
|
|
300
|
+
return { stdout: "", stderr: "" };
|
|
301
|
+
const script = [
|
|
302
|
+
'. "$1/work-mesh-enqueue.sh" 2>/dev/null || true',
|
|
303
|
+
'. "$1/work-mesh-live-hook.sh" 2>/dev/null || true',
|
|
304
|
+
'. "$1/work-mesh-live-rebind.sh" 2>/dev/null || true',
|
|
305
|
+
'if command -v work_mesh_live_write_binding_marker >/dev/null 2>&1; then work_mesh_live_write_binding_marker "$2" "$3" "$4" "$5" || true; fi',
|
|
306
|
+
].join("\n");
|
|
307
|
+
const result = deps.runCommand("bash", ["-c", script, "_", library, sessionId, company,
|
|
308
|
+
getMetaValue(metadata, "project"), getMetaValue(metadata, "task")], cwd, {
|
|
309
|
+
...deps.env,
|
|
310
|
+
HQ_ROOT: root,
|
|
311
|
+
PWD: deps.env.PWD || cwd,
|
|
312
|
+
});
|
|
313
|
+
if (result.error) {
|
|
314
|
+
const code = result.error.code;
|
|
315
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
316
|
+
return { stdout: "", stderr: "" };
|
|
317
|
+
throw result.error;
|
|
318
|
+
}
|
|
319
|
+
return { stdout: result.stdout, stderr: result.stderr };
|
|
320
|
+
}
|
|
221
321
|
function recordHqSessionWorkMeshEffects(input, deps) {
|
|
222
|
-
if (input.key === "company_slug" && input.value && input.value !== input.previous && input.value !== "personal") {
|
|
322
|
+
if ((input.key === "company_slug" || input.key === "company_slugs") && input.value && input.value !== input.previous && input.value !== "personal") {
|
|
223
323
|
const stderr = recordWorkMeshSessionStart({
|
|
224
324
|
sessionId: input.sessionId,
|
|
225
325
|
root: input.root,
|
|
@@ -443,14 +543,18 @@ function atomicWrite(filePath, contents, deps) {
|
|
|
443
543
|
throw error;
|
|
444
544
|
}
|
|
445
545
|
}
|
|
446
|
-
function mintScopeCapability(root, sessionId,
|
|
447
|
-
|
|
546
|
+
function mintScopeCapability(root, sessionId, companies, deps, includeSet = false) {
|
|
547
|
+
const company = companies[0] ?? "";
|
|
548
|
+
if (!root || !sessionId || !company || companies.some((slug) => !/^[A-Za-z0-9_-]+$/.test(slug)))
|
|
448
549
|
return false;
|
|
449
550
|
const directory = path.join(root, "workspace", "sessions", sessionId);
|
|
450
551
|
const filePath = path.join(directory, "scope-capability.json");
|
|
451
552
|
try {
|
|
452
553
|
deps.mkdir(directory);
|
|
453
|
-
const
|
|
554
|
+
const mintedAt = timestamp(deps.now());
|
|
555
|
+
const value = includeSet
|
|
556
|
+
? { session_id: sessionId, company_slug: company, minted_at: mintedAt, company_slugs: companies }
|
|
557
|
+
: { session_id: sessionId, company_slug: company, minted_at: mintedAt };
|
|
454
558
|
atomicWrite(filePath, `${JSON.stringify(value, null, 2)}\n`, deps);
|
|
455
559
|
return true;
|
|
456
560
|
}
|
|
@@ -489,7 +593,7 @@ export function executeHqSession(argv, context, overrides = {}) {
|
|
|
489
593
|
if (!subcommand || subcommand === "-h" || subcommand === "--help" || subcommand === "help") {
|
|
490
594
|
return { code: 0, stdout: HQ_SESSION_HELP, stderr: "" };
|
|
491
595
|
}
|
|
492
|
-
if (!["current", "path", "get", "set"].includes(subcommand)) {
|
|
596
|
+
if (!["current", "path", "get", "set", "add", "remove"].includes(subcommand)) {
|
|
493
597
|
return { code: 1, stdout: "", stderr: `hq-session: unknown subcommand '${subcommand}'\n${HQ_SESSION_HELP}` };
|
|
494
598
|
}
|
|
495
599
|
if (subcommand === "current") {
|
|
@@ -505,7 +609,15 @@ export function executeHqSession(argv, context, overrides = {}) {
|
|
|
505
609
|
if (!metaPath || !deps.isFile(metaPath))
|
|
506
610
|
return { code: 0, stdout: "", stderr: "" };
|
|
507
611
|
try {
|
|
508
|
-
const
|
|
612
|
+
const contents = deps.readFile(metaPath, "utf8");
|
|
613
|
+
if (key === "company_slugs") {
|
|
614
|
+
const primary = getMetaValue(contents, "company_slug");
|
|
615
|
+
const slugs = deps.multiCompanyLockEnabled
|
|
616
|
+
? metaCompanySlugs(contents).filter((slug) => slug === "personal" || exactCompanyDir(root, slug, deps))
|
|
617
|
+
: primary ? [primary] : [];
|
|
618
|
+
return { code: 0, stdout: slugs.length ? `${slugs.join(",")}\n` : "", stderr: "" };
|
|
619
|
+
}
|
|
620
|
+
const result = findMetaValue(contents, key);
|
|
509
621
|
return { code: 0, stdout: result.found ? `${result.value}\n` : "", stderr: "" };
|
|
510
622
|
}
|
|
511
623
|
catch (error) {
|
|
@@ -517,10 +629,71 @@ export function executeHqSession(argv, context, overrides = {}) {
|
|
|
517
629
|
throw error;
|
|
518
630
|
}
|
|
519
631
|
}
|
|
632
|
+
if (subcommand === "add" || subcommand === "remove") {
|
|
633
|
+
const kind = args[0] ?? "";
|
|
634
|
+
const slug = args[1] ?? "";
|
|
635
|
+
if (kind !== "company" || args.length < 2 || !slug) {
|
|
636
|
+
return { code: 1, stdout: "", stderr: `usage: hq-session.sh ${subcommand} company <slug>\n` };
|
|
637
|
+
}
|
|
638
|
+
if (subcommand === "add" && !deps.multiCompanyLockEnabled) {
|
|
639
|
+
return { code: 1, stdout: "", stderr: "hq-session: adding a company is unavailable because hooks.multi-company-session-lock is off\n" };
|
|
640
|
+
}
|
|
641
|
+
if (!/^[A-Za-z0-9_-]+$/.test(slug)) {
|
|
642
|
+
return { code: 1, stdout: "", stderr: `hq-session: invalid company_slug: '${slug}' (allowed characters: A-Z a-z 0-9 - _)\n` };
|
|
643
|
+
}
|
|
644
|
+
const dir = path.join(root, "companies", slug);
|
|
645
|
+
if (subcommand === "add" && (slug === "personal" || !isRegisteredCompany(root, slug, deps) || deps.isSymlink(dir) || hasCompanyCaseAlias(root, slug, deps) || !exactCompanyDir(root, slug, deps))) {
|
|
646
|
+
return { code: 1, stdout: "", stderr: `hq-session: company_slug must use the exact real directory name: '${slug}'\n` };
|
|
647
|
+
}
|
|
648
|
+
const sessionId = sessionOverride || resolveSessionId(root, deps);
|
|
649
|
+
if (!sessionId)
|
|
650
|
+
return { code: 1, stdout: "", stderr: "hq-session: no current session — pass --session-id <id> to target a session explicitly.\n" };
|
|
651
|
+
deps.mkdir(path.join(root, "workspace", "sessions", sessionId));
|
|
652
|
+
return withCompanyLock(root, sessionId, () => {
|
|
653
|
+
const target = path.join(root, "workspace", "sessions", sessionId, "meta.yaml");
|
|
654
|
+
try {
|
|
655
|
+
deps.mkdir(path.dirname(target));
|
|
656
|
+
const existed = deps.isFile(target);
|
|
657
|
+
const oldText = existed ? deps.readFile(target, "utf8") : `session_id: ${sessionId}\nstarted_at: "${timestamp(deps.now())}"\nsenior: user\n`;
|
|
658
|
+
const rawCompanies = metaCompanySlugs(oldText);
|
|
659
|
+
const companies = rawCompanies.filter((company) => company === "personal" || exactCompanyDir(root, company, deps));
|
|
660
|
+
if (subcommand === "add" && companies.includes(slug))
|
|
661
|
+
return { code: 0, stdout: "", stderr: "" };
|
|
662
|
+
const next = subcommand === "add"
|
|
663
|
+
? [...(companies[0] === "personal" ? [] : companies), slug]
|
|
664
|
+
: companies.filter((value) => value !== slug);
|
|
665
|
+
if (!next.length)
|
|
666
|
+
return { code: 1, stdout: "", stderr: "hq-session: cannot remove the last locked company; use set company_slug <slug> instead\n" };
|
|
667
|
+
if (subcommand === "remove" && !rawCompanies.includes(slug))
|
|
668
|
+
return { code: 0, stdout: "", stderr: "" };
|
|
669
|
+
const updatedText = writeCompanyMetadata(oldText, next);
|
|
670
|
+
atomicWrite(target, updatedText, deps);
|
|
671
|
+
if (!mintScopeCapability(root, sessionId, next, deps, true)) {
|
|
672
|
+
if (existed)
|
|
673
|
+
atomicWrite(target, oldText, deps);
|
|
674
|
+
else
|
|
675
|
+
deps.remove(target);
|
|
676
|
+
return { code: 1, stdout: "", stderr: `hq-session: failed to mint scope-capability for session ${sessionId}; metadata was restored\n` };
|
|
677
|
+
}
|
|
678
|
+
const stdout = subcommand === "add" ? emitHardPolicies(root, slug, deps) : "";
|
|
679
|
+
const primaryChanged = next[0] !== companies[0];
|
|
680
|
+
const workMeshOutput = primaryChanged
|
|
681
|
+
? registerWorkMeshCompany(root, sessionId, next[0], updatedText, context.cwd, deps)
|
|
682
|
+
: { stdout: "", stderr: "" };
|
|
683
|
+
return { code: 0, stdout: stdout + workMeshOutput.stdout, stderr: workMeshOutput.stderr };
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
687
|
+
return { code: 1, stdout: "", stderr: `${message}\n` };
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
}
|
|
520
691
|
const key = args[0] ?? "";
|
|
521
692
|
const value = args[1] ?? "";
|
|
522
693
|
if (!key || args.length < 2)
|
|
523
694
|
return { code: 1, stdout: "", stderr: "usage: hq-session.sh set <key> <value>\n" };
|
|
695
|
+
if (key === "company_slugs")
|
|
696
|
+
return { code: 1, stdout: "", stderr: "hq-session: set company_slugs is unsupported; use add/remove company to update the validated lock set\n" };
|
|
524
697
|
if (key === "company_slug" && value) {
|
|
525
698
|
if (!/^[A-Za-z0-9_-]+$/.test(value)) {
|
|
526
699
|
return { code: 1, stdout: "", stderr: `hq-session: invalid company_slug: '${value}' (allowed characters: A-Z a-z 0-9 - _)\n` };
|
|
@@ -548,35 +721,39 @@ export function executeHqSession(argv, context, overrides = {}) {
|
|
|
548
721
|
const initial = `session_id: ${sessionId}\nstarted_at: "${timestamp(deps.now())}"\nsenior: user\n`;
|
|
549
722
|
deps.writeFile(target, initial, 0o666 & ~deps.umask());
|
|
550
723
|
}
|
|
551
|
-
let previous = "";
|
|
552
|
-
try {
|
|
553
|
-
previous = getMetaValue(deps.readFile(target, "utf8"), key);
|
|
554
|
-
}
|
|
555
|
-
catch {
|
|
556
|
-
previous = "";
|
|
557
|
-
}
|
|
558
724
|
const oldText = deps.readFile(target, "utf8");
|
|
725
|
+
const previous = getMetaValue(oldText, key);
|
|
726
|
+
const companySetChanged = key === "company_slug" && value !== ""
|
|
727
|
+
&& JSON.stringify(metaCompanySlugs(oldText)) !== JSON.stringify([value]);
|
|
559
728
|
const records = oldText.split("\n");
|
|
560
729
|
if (records.at(-1) === "")
|
|
561
730
|
records.pop();
|
|
731
|
+
const clearProjectTask = key === "company_slug" && previous !== "" && value !== previous;
|
|
562
732
|
let found = false;
|
|
563
|
-
const replaced =
|
|
733
|
+
const replaced = [];
|
|
734
|
+
for (const line of records) {
|
|
564
735
|
const first = line.match(/^\s*(\S+)/)?.[1] ?? "";
|
|
565
|
-
if (first
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
736
|
+
if (clearProjectTask && (first === "project:" || first === "task:"))
|
|
737
|
+
continue;
|
|
738
|
+
if (first === `${key}:`) {
|
|
739
|
+
found = true;
|
|
740
|
+
replaced.push(`${key}: ${value}`);
|
|
741
|
+
}
|
|
742
|
+
else
|
|
743
|
+
replaced.push(line);
|
|
744
|
+
}
|
|
570
745
|
if (!found)
|
|
571
746
|
replaced.push(`${key}: ${value}`);
|
|
572
|
-
|
|
747
|
+
let updatedText = `${replaced.join("\n")}\n`;
|
|
748
|
+
if (key === "company_slug")
|
|
749
|
+
updatedText = writeCompanyMetadata(updatedText, value ? [value] : []);
|
|
573
750
|
atomicWrite(target, updatedText, deps);
|
|
574
751
|
let stdout = "";
|
|
575
|
-
if (key === "company_slug" && value &&
|
|
576
|
-
if (!mintScopeCapability(root, sessionId, value, deps)) {
|
|
752
|
+
if (key === "company_slug" && value && companySetChanged) {
|
|
753
|
+
if (!mintScopeCapability(root, sessionId, [value], deps, true)) {
|
|
577
754
|
return { code: 1, stdout: "", stderr: `hq-session: failed to mint scope-capability for session ${sessionId}\n` };
|
|
578
755
|
}
|
|
579
|
-
if (value !== "personal")
|
|
756
|
+
if (value !== previous && value !== "personal")
|
|
580
757
|
stdout = emitHardPolicies(root, value, deps);
|
|
581
758
|
}
|
|
582
759
|
const workMeshOutput = recordHqSessionWorkMeshEffects({
|
|
@@ -597,7 +774,20 @@ export function executeHqSession(argv, context, overrides = {}) {
|
|
|
597
774
|
}
|
|
598
775
|
/** Signature consumed by the lightweight fast-core dispatcher. */
|
|
599
776
|
export async function runHqSessionFast(argv, _stdin, cwd, hqRoot) {
|
|
600
|
-
const
|
|
777
|
+
const commandArgs = argv[0] === "--session-id"
|
|
778
|
+
? argv.slice(2)
|
|
779
|
+
: argv[0]?.startsWith("--session-id=") ? argv.slice(1) : argv;
|
|
780
|
+
const needsMultiCompanyFlag = commandArgs[0] === "add"
|
|
781
|
+
|| (commandArgs[0] === "get" && commandArgs[1] === "company_slugs");
|
|
782
|
+
let enabled = false;
|
|
783
|
+
if (needsMultiCompanyFlag) {
|
|
784
|
+
const [{ fastFlagEnabled }, { MULTI_COMPANY_SESSION_LOCK_FLAG }] = await Promise.all([
|
|
785
|
+
import("../native-fast-route.js"),
|
|
786
|
+
import("../lib/cli-flag-keys.js"),
|
|
787
|
+
]);
|
|
788
|
+
enabled = await fastFlagEnabled(MULTI_COMPANY_SESSION_LOCK_FLAG);
|
|
789
|
+
}
|
|
790
|
+
const result = executeHqSession(argv, { cwd, hqRoot }, { multiCompanyLockEnabled: enabled });
|
|
601
791
|
if (result.stdout)
|
|
602
792
|
process.stdout.write(result.stdout);
|
|
603
793
|
if (result.stderr)
|
|
@@ -23,6 +23,7 @@ export type ResolveCompanyDependencies = {
|
|
|
23
23
|
onHqExit?: (args: readonly string[]) => void;
|
|
24
24
|
pid?: number;
|
|
25
25
|
random?: () => number;
|
|
26
|
+
multiCompanyLockEnabled?: boolean;
|
|
26
27
|
};
|
|
27
28
|
export declare function resolveCompany(args: readonly string[], dependencies: ResolveCompanyDependencies): Promise<ResolveCompanyResult>;
|
|
28
29
|
export declare function runResolveCompanyFast(args: readonly string[], stdin: string, cwd: string, hqRoot: string | undefined, _commandName?: string, _hqRootArg?: string, stdinReader?: () => Promise<string>): Promise<number>;
|
|
@@ -4,6 +4,7 @@ import * as fs from "node:fs";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { awkCannotOpenFileDiagnostic } from "../lib/core-utils/awk-diagnostics.js";
|
|
6
6
|
import { buildCmdExeSpawnPlan, isWindowsBatchFile } from "../utils/windows-spawn.js";
|
|
7
|
+
import { fastFlagEnabled } from "../native-fast-route.js";
|
|
7
8
|
export const RESOLVE_COMPANY_ASSET = "core/scripts/resolve-company.sh";
|
|
8
9
|
export const RESOLVE_COMPANY_HELP = [
|
|
9
10
|
"hq-core: public",
|
|
@@ -143,7 +144,7 @@ function sessionId(root, env, fileSystem) {
|
|
|
143
144
|
const current = readFile(path.join(root, "workspace/sessions/.current"), fileSystem)?.replace(/[ \t\n\v\f\r]/g, "") ?? "";
|
|
144
145
|
return validSessionId(current) ? current : "";
|
|
145
146
|
}
|
|
146
|
-
function sessionCompany(root, env, fileSystem) {
|
|
147
|
+
function sessionCompany(root, env, fileSystem, multiCompanyLockEnabled = false) {
|
|
147
148
|
const helper = path.join(root, "core/scripts/hq-session.sh");
|
|
148
149
|
try {
|
|
149
150
|
fileSystem.accessSync(helper, fs.constants.X_OK);
|
|
@@ -151,19 +152,19 @@ function sessionCompany(root, env, fileSystem) {
|
|
|
151
152
|
catch (error) {
|
|
152
153
|
const code = errorCode(error);
|
|
153
154
|
if (code === "ENOENT" || code === "ENOTDIR" || code === "EACCES" || code === "ELOOP")
|
|
154
|
-
return "";
|
|
155
|
+
return { company: "", companies: [] };
|
|
155
156
|
throw error;
|
|
156
157
|
}
|
|
157
158
|
// A directory may be executable according to access(2), but `bash directory`
|
|
158
159
|
// fails without output, just as hq-session.sh does here.
|
|
159
160
|
if (!isFile(helper, fileSystem))
|
|
160
|
-
return "";
|
|
161
|
+
return { company: "", companies: [] };
|
|
161
162
|
const id = sessionId(root, env, fileSystem);
|
|
162
163
|
if (!id)
|
|
163
|
-
return "";
|
|
164
|
+
return { company: "", companies: [] };
|
|
164
165
|
const meta = readFile(path.join(root, "workspace/sessions", id, "meta.yaml"), fileSystem);
|
|
165
166
|
if (meta === undefined)
|
|
166
|
-
return "";
|
|
167
|
+
return { company: "", companies: [] };
|
|
167
168
|
let value = "";
|
|
168
169
|
for (const line of meta.split("\n")) {
|
|
169
170
|
const firstField = line.replace(/^[ \t]+/, "").split(/[ \t]+/, 1)[0];
|
|
@@ -174,7 +175,30 @@ function sessionCompany(root, env, fileSystem) {
|
|
|
174
175
|
}
|
|
175
176
|
// The shell's command substitution strips trailing newlines, then tr removes
|
|
176
177
|
// all remaining whitespace and double quotes from the helper's output.
|
|
177
|
-
|
|
178
|
+
value = value.replace(/[ \t\n\v\f\r"]/g, "");
|
|
179
|
+
if (!value || !multiCompanyLockEnabled)
|
|
180
|
+
return { company: value, companies: value ? [value] : [] };
|
|
181
|
+
let listed = "";
|
|
182
|
+
for (const line of meta.split("\n")) {
|
|
183
|
+
if (line.replace(/^[ \t]+/, "").split(/[ \t]+/, 1)[0] !== "company_slugs:")
|
|
184
|
+
continue;
|
|
185
|
+
listed = line.replace(/^[^:]+:[ \t]*/, "").replace(/^"|"$/g, "");
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
const companies = [...new Set((listed ? listed.split(",") : [value]).filter((slug) => /^[A-Za-z0-9_-]+$/.test(slug) &&
|
|
189
|
+
(slug === "personal" || (() => {
|
|
190
|
+
try {
|
|
191
|
+
return fileSystem.statSync(path.join(root, "companies", slug)).isDirectory() && !fileSystem.lstatSync(path.join(root, "companies", slug)).isSymbolicLink();
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
})())))];
|
|
197
|
+
if (companies[0] !== value)
|
|
198
|
+
return { company: "", companies: [] };
|
|
199
|
+
if (companies.length > 1 && companies.includes("personal"))
|
|
200
|
+
return { company: value, companies: [value] };
|
|
201
|
+
return { company: value, companies };
|
|
178
202
|
}
|
|
179
203
|
function fleetIdentityPresent(rootPrefix, env, fileSystem) {
|
|
180
204
|
if (env.HQ_AGENT_WORKDIR || env.HQ_AGENT_COMPANY_DIR || env.HQ_AGENT_IDENTITY_FILE)
|
|
@@ -386,8 +410,9 @@ function runHq(args, env, timeoutMs, cwd, command = "hq", onExitObserved) {
|
|
|
386
410
|
});
|
|
387
411
|
});
|
|
388
412
|
}
|
|
389
|
-
function output(company, source) {
|
|
390
|
-
|
|
413
|
+
function output(company, source, companies) {
|
|
414
|
+
const payload = { company, source, ...(source === "session" && companies ? { companies } : {}) };
|
|
415
|
+
return { code: 0, stdout: Buffer.from(`${JSON.stringify(payload)}\n`), stderr: Buffer.alloc(0) };
|
|
391
416
|
}
|
|
392
417
|
function parseArgs(args) {
|
|
393
418
|
let root = "";
|
|
@@ -519,9 +544,13 @@ export async function resolveCompany(args, dependencies) {
|
|
|
519
544
|
const promptMatch = promptSlug(prompt, slugs);
|
|
520
545
|
if (promptMatch)
|
|
521
546
|
return output(promptMatch, "prompt");
|
|
522
|
-
const sessionMatch = sessionCompany(root, env, fileSystem);
|
|
523
|
-
if (sessionMatch && knownSlugs.has(sessionMatch))
|
|
524
|
-
|
|
547
|
+
const sessionMatch = sessionCompany(root, env, fileSystem, dependencies.multiCompanyLockEnabled);
|
|
548
|
+
if (sessionMatch.company && knownSlugs.has(sessionMatch.company)) {
|
|
549
|
+
const companies = sessionMatch.companies.filter((slug) => knownSlugs.has(slug));
|
|
550
|
+
return dependencies.multiCompanyLockEnabled
|
|
551
|
+
? output(sessionMatch.company, "session", companies.length ? companies : [sessionMatch.company])
|
|
552
|
+
: output(sessionMatch.company, "session");
|
|
553
|
+
}
|
|
525
554
|
if (fleetIdentityPresent(env.HQ_AGENT_ROOT_PREFIX ?? "", env, fileSystem))
|
|
526
555
|
return output("", "none");
|
|
527
556
|
// jq is intentionally no longer needed. The native route is always hosted by
|
|
@@ -574,6 +603,7 @@ export async function runResolveCompanyFast(args, stdin, cwd, hqRoot, _commandNa
|
|
|
574
603
|
hqRoot,
|
|
575
604
|
stdin: actualStdin,
|
|
576
605
|
stdinIsTty: process.stdin.isTTY === true,
|
|
606
|
+
multiCompanyLockEnabled: await fastFlagEnabled("hooks.multi-company-session-lock"),
|
|
577
607
|
});
|
|
578
608
|
process.stdout.write(result.stdout);
|
|
579
609
|
if (result.stderr.length)
|
|
@@ -4,6 +4,8 @@ import { collectFeedbackLogs, feedbackLogsEnabled } from "../utils/feedback-logs
|
|
|
4
4
|
import { type LogBundleRef } from "../utils/feedback-log-bundle.js";
|
|
5
5
|
export declare const FEEDBACK_LATEST_VERSION_FLAG = "feedback.latest-version";
|
|
6
6
|
export declare const FEEDBACK_LATEST_VERSION_FLAG_FALLBACK = false;
|
|
7
|
+
export declare const FEEDBACK_REPORTER_KIND_FLAG = "feedback.reporter-kind";
|
|
8
|
+
export declare const FEEDBACK_REPORTER_KIND_FLAG_FALLBACK = true;
|
|
7
9
|
export declare const BODY_MAX_BYTES: number;
|
|
8
10
|
/** Validate fields that must be valid before feedback attachments are uploaded. */
|
|
9
11
|
export declare function validateFeedbackContent(title: string, body: string): void;
|
|
@@ -44,7 +46,15 @@ export interface FeedbackSubmitOptions {
|
|
|
44
46
|
diagnostics?: DiagnosticsBlob;
|
|
45
47
|
/** Precomputed setting so environment lookup happens before attachments upload. */
|
|
46
48
|
logsEnabled?: boolean;
|
|
49
|
+
/** Optional one-way override: callers can mark human sessions as agent. */
|
|
50
|
+
reporterOverride?: "agent";
|
|
47
51
|
}
|
|
52
|
+
export interface FeedbackReporterMark {
|
|
53
|
+
reporter_kind: "agent" | "human";
|
|
54
|
+
reporter_source: string;
|
|
55
|
+
}
|
|
56
|
+
/** Resolve a safe reporter classification without returning any env values. */
|
|
57
|
+
export declare function resolveFeedbackReporter(env?: NodeJS.ProcessEnv, isTTY?: boolean, override?: string): FeedbackReporterMark;
|
|
48
58
|
export declare function readBodyFile(bodyFile: string, stdin?: NodeJS.ReadableStream): Promise<string>;
|
|
49
59
|
/**
|
|
50
60
|
* Attach redacted `~/.hq` log tails to `diagnostics` if, and only if, they fit
|
|
@@ -10,6 +10,8 @@ import { resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
|
10
10
|
import { cachedLatestVersion } from "../utils/version-check.js";
|
|
11
11
|
export const FEEDBACK_LATEST_VERSION_FLAG = "feedback.latest-version";
|
|
12
12
|
export const FEEDBACK_LATEST_VERSION_FLAG_FALLBACK = false;
|
|
13
|
+
export const FEEDBACK_REPORTER_KIND_FLAG = "feedback.reporter-kind";
|
|
14
|
+
export const FEEDBACK_REPORTER_KIND_FLAG_FALLBACK = true;
|
|
13
15
|
export const BODY_MAX_BYTES = 64 * 1024;
|
|
14
16
|
/** Validate fields that must be valid before feedback attachments are uploaded. */
|
|
15
17
|
export function validateFeedbackContent(title, body) {
|
|
@@ -46,6 +48,32 @@ function validateFeedbackRequestSize(requestBody) {
|
|
|
46
48
|
* after attaching, so this reserve only needs to be roughly right.
|
|
47
49
|
*/
|
|
48
50
|
const LOGS_ENVELOPE_RESERVE_BYTES = 1024;
|
|
51
|
+
const AGENT_RUNTIME_MARKERS = [
|
|
52
|
+
"HQ_LANE_ID",
|
|
53
|
+
"HQ_POLICY_WORKER_DIR",
|
|
54
|
+
"HQ_AGENT_DIR",
|
|
55
|
+
"CODEX_THREAD_ID",
|
|
56
|
+
"CODEX_SESSION_ID",
|
|
57
|
+
"CLAUDE_CODE_SESSION_ID",
|
|
58
|
+
"CLAUDE_SESSION_ID",
|
|
59
|
+
"HQ_SESSION_ID",
|
|
60
|
+
"GROK_SESSION_ID",
|
|
61
|
+
];
|
|
62
|
+
/** Resolve a safe reporter classification without returning any env values. */
|
|
63
|
+
export function resolveFeedbackReporter(env = process.env, isTTY = process.stdin.isTTY === true, override) {
|
|
64
|
+
for (const marker of AGENT_RUNTIME_MARKERS) {
|
|
65
|
+
if (env[marker]?.trim()) {
|
|
66
|
+
return { reporter_kind: "agent", reporter_source: marker };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (override === "agent") {
|
|
70
|
+
return { reporter_kind: "agent", reporter_source: "CLI_REPORTER_OVERRIDE" };
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
reporter_kind: "human",
|
|
74
|
+
reporter_source: isTTY ? "stdin_isTTY" : "no_agent_runtime_marker",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
49
77
|
function buildFeedbackRequestBody(opts) {
|
|
50
78
|
const requestBody = {
|
|
51
79
|
type: opts.type,
|
|
@@ -53,6 +81,10 @@ function buildFeedbackRequestBody(opts) {
|
|
|
53
81
|
body: opts.body,
|
|
54
82
|
diagnostics: opts.diagnostics,
|
|
55
83
|
};
|
|
84
|
+
if (opts.reporterMark) {
|
|
85
|
+
requestBody.reporter_kind = opts.reporterMark.reporter_kind;
|
|
86
|
+
requestBody.reporter_source = opts.reporterMark.reporter_source;
|
|
87
|
+
}
|
|
56
88
|
if (opts.company) {
|
|
57
89
|
requestBody.company = opts.company;
|
|
58
90
|
}
|
|
@@ -124,6 +156,9 @@ export async function submitFeedback(opts) {
|
|
|
124
156
|
// which invokes the same validator before uploading any attachments.
|
|
125
157
|
validateFeedbackContent(opts.title, opts.body);
|
|
126
158
|
const diagnostics = opts.diagnostics ?? collectDiagnostics();
|
|
159
|
+
const reporterMark = opts.type === "bug" && resolveProcessFlagGate(FEEDBACK_REPORTER_KIND_FLAG, { fallback: FEEDBACK_REPORTER_KIND_FLAG_FALLBACK }, () => FEEDBACK_REPORTER_KIND_FLAG_FALLBACK)
|
|
160
|
+
? resolveFeedbackReporter(process.env, process.stdin.isTTY === true, opts.reporterOverride)
|
|
161
|
+
: undefined;
|
|
127
162
|
const requestBody = buildFeedbackRequestBody({
|
|
128
163
|
type: opts.type,
|
|
129
164
|
title: opts.title,
|
|
@@ -132,6 +167,7 @@ export async function submitFeedback(opts) {
|
|
|
132
167
|
diagnostics,
|
|
133
168
|
screenshots: opts.screenshots,
|
|
134
169
|
logBundle: opts.logBundle,
|
|
170
|
+
reporterMark,
|
|
135
171
|
});
|
|
136
172
|
// Direct callers can supply attachment references; ensure the complete base
|
|
137
173
|
// envelope still fits before adding optional inline logs or making a request.
|
|
@@ -172,12 +208,16 @@ function registerSubcommand(feedbackCmd, type) {
|
|
|
172
208
|
.requiredOption("--title <text>", "Short title for the report")
|
|
173
209
|
.requiredOption("--body-file <path>", "Path to a markdown file with the body; use - to read from stdin")
|
|
174
210
|
.option("--company <slug>", "Company slug to associate with the report")
|
|
211
|
+
.option("--reporter <kind>", "Mark this bug report as agent-filed (only 'agent' is accepted)")
|
|
175
212
|
.option("--screenshot <path>", `Attach a screenshot (repeatable, up to ${MAX_SCREENSHOTS}; .png/.jpg/.jpeg/.webp/.gif)`, (value, prev) => [...prev, value], [])
|
|
176
213
|
// Commander turns `--no-logs` into a boolean `logs` that defaults to true,
|
|
177
214
|
// so the default is "attach logs" and the flag is the opt-out.
|
|
178
215
|
.option("--no-logs", "Do not attach redacted tails of your ~/.hq log files (also: HQ_FEEDBACK_LOGS=0)")
|
|
179
216
|
.action(async (opts) => {
|
|
180
217
|
try {
|
|
218
|
+
if (opts.reporter !== undefined && (opts.reporter !== "agent" || type !== "bug")) {
|
|
219
|
+
throw new Error("Invalid --reporter value: only bug reports may use 'agent'.");
|
|
220
|
+
}
|
|
181
221
|
const token = await ensureCognitoToken({ interactive: false });
|
|
182
222
|
const body = await readBodyFile(opts.bodyFile);
|
|
183
223
|
validateFeedbackContent(opts.title, body);
|
|
@@ -192,6 +232,9 @@ function registerSubcommand(feedbackCmd, type) {
|
|
|
192
232
|
diagnostics.latestVersion = latestVersion;
|
|
193
233
|
}
|
|
194
234
|
const logsEnabled = opts.logs !== false && feedbackLogsEnabled();
|
|
235
|
+
const reporterMark = type === "bug" && resolveProcessFlagGate(FEEDBACK_REPORTER_KIND_FLAG, { fallback: FEEDBACK_REPORTER_KIND_FLAG_FALLBACK }, () => FEEDBACK_REPORTER_KIND_FLAG_FALLBACK)
|
|
236
|
+
? resolveFeedbackReporter(process.env, process.stdin.isTTY === true, opts.reporter)
|
|
237
|
+
: undefined;
|
|
195
238
|
// Validate the serialized server request envelope before screenshots
|
|
196
239
|
// or the log bundle can create remote objects. Attachment references
|
|
197
240
|
// are added and checked again inside submitFeedback after upload.
|
|
@@ -201,6 +244,7 @@ function registerSubcommand(feedbackCmd, type) {
|
|
|
201
244
|
body,
|
|
202
245
|
company: opts.company,
|
|
203
246
|
diagnostics,
|
|
247
|
+
reporterMark,
|
|
204
248
|
}));
|
|
205
249
|
// vaultApiFetch can reject an hqk_ token or an invalid API base URL
|
|
206
250
|
// locally before fetch. Check the mandatory POST, and the screenshot
|
|
@@ -236,6 +280,7 @@ function registerSubcommand(feedbackCmd, type) {
|
|
|
236
280
|
includeLogs: opts.logs !== false,
|
|
237
281
|
logBundle,
|
|
238
282
|
diagnostics,
|
|
283
|
+
reporterOverride: opts.reporter === "agent" ? "agent" : undefined,
|
|
239
284
|
logsEnabled,
|
|
240
285
|
});
|
|
241
286
|
console.log(`Submitted: ${result.id}`);
|