@indigoai-us/hq-cli 5.345.103 → 5.345.105

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 (74) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/assets/bot-workers/setup/skills/add-a-pack.md +130 -0
  3. package/assets/bot-workers/setup/worker.yaml +3 -0
  4. package/dist/command-catalog.generated.d.ts +206 -0
  5. package/dist/command-catalog.generated.js +246 -0
  6. package/dist/commands/bot.d.ts +10 -0
  7. package/dist/commands/bot.js +20 -12
  8. package/dist/commands/daemon.js +385 -1
  9. package/dist/commands/integrations-connect.d.ts +6 -0
  10. package/dist/commands/integrations-connect.js +36 -0
  11. package/dist/commands/integrations-slack.d.ts +30 -0
  12. package/dist/commands/integrations-slack.js +81 -0
  13. package/dist/commands/integrations.js +2 -0
  14. package/dist/commands/pack-install.d.ts +20 -1
  15. package/dist/commands/pack-install.js +80 -5
  16. package/dist/commands/pack-start-bot.d.ts +24 -0
  17. package/dist/commands/pack-start-bot.js +76 -0
  18. package/dist/commands/packs.d.ts +9 -0
  19. package/dist/commands/packs.js +26 -3
  20. package/dist/commands/pkg-install.js +11 -2
  21. package/dist/commands/secrets.d.ts +1 -5
  22. package/dist/commands/secrets.js +2 -92
  23. package/dist/lib/bot/config.d.ts +7 -0
  24. package/dist/lib/bot/daemon.js +2 -1
  25. package/dist/lib/bot/index.d.ts +2 -0
  26. package/dist/lib/bot/index.js +2 -0
  27. package/dist/lib/bot/lean-settings.d.ts +16 -0
  28. package/dist/lib/bot/lean-settings.js +34 -0
  29. package/dist/lib/bot/pack-link.d.ts +66 -0
  30. package/dist/lib/bot/pack-link.js +153 -0
  31. package/dist/lib/bot/pack-setup-driver.d.ts +37 -0
  32. package/dist/lib/bot/pack-setup-driver.js +83 -0
  33. package/dist/lib/bot/pack-setup.d.ts +62 -0
  34. package/dist/lib/bot/pack-setup.js +103 -0
  35. package/dist/lib/bot/prompt.d.ts +8 -0
  36. package/dist/lib/bot/prompt.js +5 -0
  37. package/dist/lib/bot/run.d.ts +18 -2
  38. package/dist/lib/bot/run.js +163 -25
  39. package/dist/lib/bot/runtime/claude.d.ts +27 -2
  40. package/dist/lib/bot/runtime/claude.js +49 -4
  41. package/dist/lib/bot/runtime/types.d.ts +5 -0
  42. package/dist/lib/bot/runtime-failure.d.ts +18 -2
  43. package/dist/lib/bot/runtime-failure.js +21 -1
  44. package/dist/lib/bot/toolchain.d.ts +25 -0
  45. package/dist/lib/bot/toolchain.js +33 -0
  46. package/dist/lib/bot/worker-source.d.ts +1 -0
  47. package/dist/lib/bot/worker-source.js +7 -1
  48. package/dist/lib/daemon/catalog.d.ts +15 -0
  49. package/dist/lib/daemon/catalog.js +49 -0
  50. package/dist/lib/daemon/paths.d.ts +1 -0
  51. package/dist/lib/daemon/paths.js +1 -0
  52. package/dist/lib/daemon/remote-control-forwarder.d.ts +14 -0
  53. package/dist/lib/daemon/remote-control-forwarder.js +104 -0
  54. package/dist/lib/daemon/remote-control-pools.d.ts +44 -0
  55. package/dist/lib/daemon/remote-control-pools.js +245 -0
  56. package/dist/lib/daemon/remote-control.d.ts +4 -0
  57. package/dist/lib/daemon/remote-control.js +17 -2
  58. package/dist/lib/daemon/run.js +126 -0
  59. package/dist/lib/doctor/fix/refresh-sync-progress.d.ts +5 -4
  60. package/dist/lib/doctor/fix/refresh-sync-progress.js +5 -4
  61. package/dist/lib/doctor/json-output.d.ts +19 -7
  62. package/dist/lib/doctor/json-output.js +16 -6
  63. package/dist/lib/packages/setup-block.d.ts +43 -0
  64. package/dist/lib/packages/setup-block.js +236 -0
  65. package/dist/lib/packages/setup-prompt.d.ts +34 -0
  66. package/dist/lib/packages/setup-prompt.js +128 -0
  67. package/dist/lib/packages/setup-takeover.d.ts +50 -0
  68. package/dist/lib/packages/setup-takeover.js +75 -0
  69. package/dist/lib/packages/start-bot.d.ts +47 -0
  70. package/dist/lib/packages/start-bot.js +66 -0
  71. package/dist/lib/session-company-lock.d.ts +7 -0
  72. package/dist/lib/session-company-lock.js +97 -0
  73. package/dist/types.d.ts +82 -0
  74. package/package.json +1 -1
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Recognize a content pack in what a person wrote to the setup bot (US-004,
3
+ * contract section 4), so the bot's skill text (assets/bot-workers/setup/
4
+ * skills/add-a-pack.md) and the code agree on what counts as a pack reference:
5
+ *
6
+ * https://www.hqforwork.com/packs/{slug} a pack page
7
+ * hqforwork.com/packs/{slug} the same without scheme or www
8
+ * hq-pack-{slug} the package name
9
+ * {slug} a bare slug the catalog knows
10
+ * "add the {Display Name}" a display name from the catalog
11
+ * ./packs/hq-pack-{slug}, ~/Downloads/hq-pack-{slug} a local pack folder
12
+ *
13
+ * The catalog is the curated list `hq packages list` prints (core.yaml
14
+ * `recommended_packages`, see readRecommendedPackages). A slug or display name
15
+ * that matches none of its entries is `unknown`, with the three closest
16
+ * catalog names to offer. Nothing here knows any particular pack.
17
+ */
18
+ import type { CatalogEntry } from "../../utils/hq-core-yaml.js";
19
+ /** The package-name prefix every HQ content pack carries. */
20
+ export declare const HQ_PACK_PREFIX = "hq-pack-";
21
+ /** Same shape the deep link and the pack page accept. */
22
+ export declare const PACK_SLUG_PATTERN: RegExp;
23
+ export interface PackCatalogEntry {
24
+ /** `design-styles` */
25
+ slug: string;
26
+ /** `Design Styles` */
27
+ displayName: string;
28
+ description?: string;
29
+ /** The install source as the curated catalog spells it, when it has one. */
30
+ source?: string;
31
+ }
32
+ export type PackReferenceKind = "url" | "package-name" | "slug" | "display-name" | "path";
33
+ export type PackReference = {
34
+ kind: PackReferenceKind;
35
+ slug: string;
36
+ displayName: string;
37
+ /** What to hand `hq packages install`: the slug, or the local path when the message carried one. */
38
+ installSource: string;
39
+ entry?: PackCatalogEntry;
40
+ } | {
41
+ kind: "unknown";
42
+ /** The slug or name the person seems to mean. */
43
+ query: string;
44
+ /** Up to three catalog display names closest to the query. */
45
+ suggestions: string[];
46
+ };
47
+ /** `github:indigoai-us/hq-packages#packages/hq-pack-design-styles` -> `design-styles`. */
48
+ export declare function packSlugFromSource(source: string): string | null;
49
+ /** `design-styles` -> `Design Styles`. */
50
+ export declare function displayNameFromSlug(slug: string): string;
51
+ /** The curated catalog (`recommended_packages`) as pack entries; a repeated slug keeps its first entry. */
52
+ export declare function packCatalogFromRecommended(entries: readonly CatalogEntry[]): PackCatalogEntry[];
53
+ /** The `n` catalog display names closest to `query` (slug or name), best first. */
54
+ export declare function closestPackNames(query: string, catalog: readonly PackCatalogEntry[], n?: number): string[];
55
+ /**
56
+ * The pack a message refers to, or null when it mentions no pack. A URL,
57
+ * package name or path always resolves (the slug is explicit); a bare slug or
58
+ * a display name must be in the catalog, otherwise the result is `unknown`
59
+ * with the closest catalog names.
60
+ */
61
+ export declare function recognizePackReference(message: string, catalog: readonly PackCatalogEntry[]): PackReference | null;
62
+ /** The suggestions the confirmation message carries: `["Add the {displayName}","Not now"]`. */
63
+ export declare function packConfirmSuggestions(displayName: string): [string, string];
64
+ /** The exact command the setup bot runs after "Add the ...". */
65
+ export declare function packInstallCommand(installSource: string): string;
66
+ //# sourceMappingURL=pack-link.d.ts.map
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Recognize a content pack in what a person wrote to the setup bot (US-004,
3
+ * contract section 4), so the bot's skill text (assets/bot-workers/setup/
4
+ * skills/add-a-pack.md) and the code agree on what counts as a pack reference:
5
+ *
6
+ * https://www.hqforwork.com/packs/{slug} a pack page
7
+ * hqforwork.com/packs/{slug} the same without scheme or www
8
+ * hq-pack-{slug} the package name
9
+ * {slug} a bare slug the catalog knows
10
+ * "add the {Display Name}" a display name from the catalog
11
+ * ./packs/hq-pack-{slug}, ~/Downloads/hq-pack-{slug} a local pack folder
12
+ *
13
+ * The catalog is the curated list `hq packages list` prints (core.yaml
14
+ * `recommended_packages`, see readRecommendedPackages). A slug or display name
15
+ * that matches none of its entries is `unknown`, with the three closest
16
+ * catalog names to offer. Nothing here knows any particular pack.
17
+ */
18
+ /** The package-name prefix every HQ content pack carries. */
19
+ export const HQ_PACK_PREFIX = "hq-pack-";
20
+ /** Same shape the deep link and the pack page accept. */
21
+ export const PACK_SLUG_PATTERN = /^[a-z0-9-]{2,64}$/;
22
+ /** `github:indigoai-us/hq-packages#packages/hq-pack-design-styles` -> `design-styles`. */
23
+ export function packSlugFromSource(source) {
24
+ const trimmed = source.trim().replace(/[/\\]+$/, "");
25
+ const last = trimmed.split(/[/\\#:]/).pop() ?? "";
26
+ const bare = last.replace(/@.*$/, "").replace(/\.(?:tgz|tar\.gz|zip)$/i, "");
27
+ const slug = bare.startsWith(HQ_PACK_PREFIX) ? bare.slice(HQ_PACK_PREFIX.length) : bare;
28
+ return PACK_SLUG_PATTERN.test(slug) ? slug : null;
29
+ }
30
+ /** `design-styles` -> `Design Styles`. */
31
+ export function displayNameFromSlug(slug) {
32
+ return slug
33
+ .split("-")
34
+ .filter(Boolean)
35
+ .map((w) => w[0].toUpperCase() + w.slice(1))
36
+ .join(" ");
37
+ }
38
+ /** The curated catalog (`recommended_packages`) as pack entries; a repeated slug keeps its first entry. */
39
+ export function packCatalogFromRecommended(entries) {
40
+ const out = new Map();
41
+ for (const entry of entries) {
42
+ const slug = packSlugFromSource(entry.source);
43
+ if (!slug || out.has(slug))
44
+ continue;
45
+ const named = entry.name?.trim();
46
+ out.set(slug, {
47
+ slug,
48
+ displayName: named || displayNameFromSlug(slug),
49
+ ...(entry.description ? { description: entry.description } : {}),
50
+ source: entry.source,
51
+ });
52
+ }
53
+ return [...out.values()];
54
+ }
55
+ const URL_PATTERN = /(?:https?:\/\/)?(?:www\.)?hqforwork\.com\/packs\/([a-z0-9-]{2,64})(?![a-z0-9-])/i;
56
+ const PACKAGE_NAME_PATTERN = /(?<![a-z0-9-])hq-pack-([a-z0-9-]{2,64})(?![a-z0-9-])/i;
57
+ /** A path token whose last `hq-pack-*` segment names the pack. */
58
+ const PATH_PATTERN = /(?:^|\s)((?:~|\.{1,2})?\/[^\s"'`]*hq-pack-([a-z0-9-]{2,64})(?:\/[^\s"'`]*)?)/i;
59
+ /** Something that looks like a slug the person typed on purpose. */
60
+ const SLUG_TOKEN = /(?<![a-z0-9-])([a-z0-9]+(?:-[a-z0-9]+)+)(?![a-z0-9-])/gi;
61
+ const PACK_TALK = /\bpacks?\b|\binstall\b|\badd\b/i;
62
+ function normalize(s) {
63
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
64
+ }
65
+ function wordPhrase(message, phrase) {
66
+ const words = normalize(phrase).split(" ").filter(Boolean);
67
+ if (words.length === 0)
68
+ return false;
69
+ const re = new RegExp(`(?<![a-z0-9])${words.map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\s-]+")}(?![a-z0-9])`, "i");
70
+ return re.test(message);
71
+ }
72
+ function found(kind, slug, catalog, installSource = slug) {
73
+ const entry = catalog.find((e) => e.slug === slug);
74
+ return { kind, slug, displayName: entry?.displayName ?? displayNameFromSlug(slug), installSource, ...(entry ? { entry } : {}) };
75
+ }
76
+ function levenshtein(a, b) {
77
+ const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
78
+ for (let i = 1; i <= a.length; i++) {
79
+ let diag = prev[0];
80
+ prev[0] = i;
81
+ for (let j = 1; j <= b.length; j++) {
82
+ const tmp = prev[j];
83
+ prev[j] = Math.min(prev[j] + 1, prev[j - 1] + 1, diag + (a[i - 1] === b[j - 1] ? 0 : 1));
84
+ diag = tmp;
85
+ }
86
+ }
87
+ return prev[b.length];
88
+ }
89
+ /** The `n` catalog display names closest to `query` (slug or name), best first. */
90
+ export function closestPackNames(query, catalog, n = 3) {
91
+ const q = normalize(query);
92
+ const scored = catalog.map((entry) => {
93
+ const candidates = [normalize(entry.slug), normalize(entry.displayName)];
94
+ const score = Math.min(...candidates.map((c) => levenshtein(q, c) / Math.max(q.length, c.length, 1)));
95
+ return { name: entry.displayName, score };
96
+ });
97
+ scored.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name));
98
+ return scored.slice(0, n).map((s) => s.name);
99
+ }
100
+ /**
101
+ * The pack a message refers to, or null when it mentions no pack. A URL,
102
+ * package name or path always resolves (the slug is explicit); a bare slug or
103
+ * a display name must be in the catalog, otherwise the result is `unknown`
104
+ * with the closest catalog names.
105
+ */
106
+ export function recognizePackReference(message, catalog) {
107
+ const text = message.trim();
108
+ if (!text)
109
+ return null;
110
+ const url = URL_PATTERN.exec(text);
111
+ if (url)
112
+ return found("url", url[1].toLowerCase(), catalog);
113
+ const localPath = PATH_PATTERN.exec(text);
114
+ if (localPath)
115
+ return found("path", localPath[2].toLowerCase(), catalog, localPath[1]);
116
+ const pkg = PACKAGE_NAME_PATTERN.exec(text);
117
+ if (pkg)
118
+ return found("package-name", pkg[1].toLowerCase(), catalog);
119
+ // A slug typed as such (hyphens and all; a one-word slug is just its name),
120
+ // then a display name in plain words; the longest wins when one contains another.
121
+ const bySlug = [...catalog]
122
+ .filter((e) => e.slug.includes("-"))
123
+ .sort((a, b) => b.slug.length - a.slug.length)
124
+ .find((e) => new RegExp(`(?<![a-z0-9-])${e.slug}(?![a-z0-9-])`, "i").test(text));
125
+ if (bySlug)
126
+ return found("slug", bySlug.slug, catalog);
127
+ const byName = [...catalog].sort((a, b) => b.displayName.length - a.displayName.length).find((e) => wordPhrase(text, e.displayName));
128
+ if (byName)
129
+ return found("display-name", byName.slug, catalog);
130
+ // A slug-shaped token the catalog does not know, in a message about packs.
131
+ if (PACK_TALK.test(text)) {
132
+ const tokens = [...text.matchAll(SLUG_TOKEN)].map((m) => m[1].toLowerCase()).filter((t) => PACK_SLUG_PATTERN.test(t));
133
+ const query = tokens[0] ?? quotedName(text);
134
+ if (query)
135
+ return { kind: "unknown", query, suggestions: closestPackNames(query, catalog) };
136
+ }
137
+ return null;
138
+ }
139
+ /** `add the "Growth Kit" pack` -> `Growth Kit`. */
140
+ function quotedName(text) {
141
+ const m = /["'“‘]([^"'”’]{2,64})["'”’]/.exec(text);
142
+ return m ? m[1].trim() : null;
143
+ }
144
+ /** The suggestions the confirmation message carries: `["Add the {displayName}","Not now"]`. */
145
+ export function packConfirmSuggestions(displayName) {
146
+ return [`Add the ${displayName}`, "Not now"];
147
+ }
148
+ /** The exact command the setup bot runs after "Add the ...". */
149
+ export function packInstallCommand(installSource) {
150
+ const arg = /[\s"']/.test(installSource) ? JSON.stringify(installSource) : installSource;
151
+ return `hq packages install ${arg} --start-bot`;
152
+ }
153
+ //# sourceMappingURL=pack-link.js.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The instant first question of a pack bot. On first start the runtime sends
3
+ * the pack's first `setup:` question from code, with the company's name to
4
+ * confirm when the question asks for it, so the person is not left waiting on
5
+ * a model turn that only works out which company they are in. Every answer
6
+ * after that goes to the model, which runs the setup as an interview.
7
+ *
8
+ * State lives in the bot folder (`pack-setup-state.json`). The next model turn
9
+ * in the owner DM gets a note that the first question already went out.
10
+ */
11
+ import type { PackSetup, PackSetupQuestion } from "../../types.js";
12
+ export declare const PACK_SETUP_STATE_FILE = "pack-setup-state.json";
13
+ export interface PackSetupState {
14
+ v: 1;
15
+ pack: string;
16
+ /** Company slug the pack is set up in. */
17
+ firm: string;
18
+ /** The question id the app asked from code. */
19
+ askedFirst: string;
20
+ /** The next model turn in the owner DM gets a note that the first question went out. */
21
+ modelNotePending: boolean;
22
+ updatedAt: string;
23
+ }
24
+ export declare function statePath(botDir: string): string;
25
+ export declare function readPackSetupState(botDir: string): PackSetupState | null;
26
+ export declare function writePackSetupState(botDir: string, state: PackSetupState): void;
27
+ /** The company's display name from companies/manifest.yaml, else its slug. */
28
+ export declare function companyDisplayName(hqRoot: string, slug: string): string;
29
+ /** The value a question proposes from `default_from`, if any. */
30
+ export declare function questionDefault(q: PackSetupQuestion, ctx: {
31
+ firmName: string;
32
+ }): string | null;
33
+ /** One question as a message: the text and its buttons, proposing the default when there is one. */
34
+ export declare function questionMessage(q: PackSetupQuestion, defaultValue: string | null): string;
35
+ /** The note on the first model turn after the app asked the first question. */
36
+ export declare function firstQuestionNote(setup: PackSetup, state: PackSetupState, firmName: string): string;
37
+ //# sourceMappingURL=pack-setup-driver.d.ts.map
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The instant first question of a pack bot. On first start the runtime sends
3
+ * the pack's first `setup:` question from code, with the company's name to
4
+ * confirm when the question asks for it, so the person is not left waiting on
5
+ * a model turn that only works out which company they are in. Every answer
6
+ * after that goes to the model, which runs the setup as an interview.
7
+ *
8
+ * State lives in the bot folder (`pack-setup-state.json`). The next model turn
9
+ * in the owner DM gets a note that the first question already went out.
10
+ */
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import * as yaml from "js-yaml";
14
+ import { questionSuggestions, suggestionsEnvelope } from "../packages/setup-prompt.js";
15
+ export const PACK_SETUP_STATE_FILE = "pack-setup-state.json";
16
+ export function statePath(botDir) {
17
+ return path.join(botDir, PACK_SETUP_STATE_FILE);
18
+ }
19
+ export function readPackSetupState(botDir) {
20
+ try {
21
+ const raw = JSON.parse(fs.readFileSync(statePath(botDir), "utf8"));
22
+ if (raw?.v !== 1 || typeof raw.pack !== "string" || typeof raw.firm !== "string" || typeof raw.askedFirst !== "string")
23
+ return null;
24
+ return {
25
+ v: 1,
26
+ pack: raw.pack,
27
+ firm: raw.firm,
28
+ askedFirst: raw.askedFirst,
29
+ modelNotePending: raw.modelNotePending === true,
30
+ updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : new Date(0).toISOString(),
31
+ };
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ export function writePackSetupState(botDir, state) {
38
+ fs.mkdirSync(botDir, { recursive: true, mode: 0o700 });
39
+ const tmp = `${statePath(botDir)}.tmp`;
40
+ fs.writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
41
+ fs.renameSync(tmp, statePath(botDir));
42
+ }
43
+ /** The company's display name from companies/manifest.yaml, else its slug. */
44
+ export function companyDisplayName(hqRoot, slug) {
45
+ try {
46
+ const doc = yaml.load(fs.readFileSync(path.join(hqRoot, "companies", "manifest.yaml"), "utf8"));
47
+ for (const table of [doc?.companies, doc]) {
48
+ const entry = table && typeof table === "object" ? table[slug] : undefined;
49
+ const name = entry && typeof entry === "object" ? entry.name : undefined;
50
+ if (typeof name === "string" && name.trim())
51
+ return name.trim();
52
+ }
53
+ }
54
+ catch {
55
+ // fall through to the slug
56
+ }
57
+ return slug;
58
+ }
59
+ /** The value a question proposes from `default_from`, if any. */
60
+ export function questionDefault(q, ctx) {
61
+ if (q.default_from === "company_display_name")
62
+ return ctx.firmName;
63
+ return null;
64
+ }
65
+ /** One question as a message: the text and its buttons, proposing the default when there is one. */
66
+ export function questionMessage(q, defaultValue) {
67
+ let text = q.text;
68
+ const items = questionSuggestions(q);
69
+ if (defaultValue) {
70
+ text = `${text} I have "${defaultValue}" from your company. Use that, or type a different one.`;
71
+ items.unshift(`Use ${defaultValue}`);
72
+ }
73
+ return items.length ? `${text}\n\n${suggestionsEnvelope(items)}` : text;
74
+ }
75
+ /** The note on the first model turn after the app asked the first question. */
76
+ export function firstQuestionNote(setup, state, firmName) {
77
+ const q = setup.questions.find((x) => x.id === state.askedFirst);
78
+ return (`[Pack setup: the app already sent your hello and your first setup question (\`${state.askedFirst}\`` +
79
+ (q?.default_from ? `, proposing "${firmName}"` : "") +
80
+ `) for the company ${state.firm} (${firmName}). HQ is signed in and that company is on this computer, so skip any checks of that. ` +
81
+ `The message below is the person's reply to that question; carry on the setup from it.]`);
82
+ }
83
+ //# sourceMappingURL=pack-setup-driver.js.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * A bot whose worker belongs to an installed pack with a `setup:` block gets
3
+ * an auto-generated "Pack setup" section in its system prompt, and, when the
4
+ * pack asks for it and HQ's own setup is unfinished, a takeover section with
5
+ * HQ's setup worker skills (build contract section 3).
6
+ *
7
+ * scan-packages links a pack worker at `core/workers/public/<id>` ->
8
+ * `core/packages/<pack>/workers/<id>`, so the pack is found by resolving the
9
+ * worker folder's real path and checking whether it sits under
10
+ * `<hqRoot>/core/packages/<pack>/`.
11
+ */
12
+ import type { PackSetup } from "../../types.js";
13
+ import { type WorkerSkillSource } from "./prompt.js";
14
+ export interface PackSetupResolveFs {
15
+ realpathSync: (p: string) => string;
16
+ existsSync: (p: string) => boolean;
17
+ }
18
+ /** The installed pack (name + root) a worker folder belongs to, or null. */
19
+ export declare function findPackForWorker(hqRoot: string, workerDir: string, io?: PackSetupResolveFs): {
20
+ packName: string;
21
+ packRoot: string;
22
+ } | null;
23
+ export interface PackSetupPromptSections {
24
+ packName: string;
25
+ /** Absolute installed pack root. */
26
+ packRoot: string;
27
+ /** The validated `setup:` block. */
28
+ setup: PackSetup;
29
+ /** "## Finish HQ setup first", present only when the bot takes over HQ setup. */
30
+ takeover?: string;
31
+ /** "## Pack setup (...)". */
32
+ packSetup: string;
33
+ }
34
+ export interface ResolvePackSetupPromptInput {
35
+ hqRoot: string;
36
+ /** hqRoot-relative worker folder (bot.json `workerDir`). */
37
+ workerDir: string;
38
+ /** Only worker-sourced bots can belong to a pack. */
39
+ workerSource?: "scaffold" | "worker";
40
+ /** Seams for tests. */
41
+ io?: PackSetupResolveFs;
42
+ readProgressNote?: () => {
43
+ path: string | null;
44
+ note: string | null;
45
+ };
46
+ setupWorkerSkills?: (hqRoot: string) => {
47
+ workerDir: string;
48
+ skills: WorkerSkillSource[];
49
+ } | null;
50
+ log?: (level: "info" | "warn", message: string) => void;
51
+ }
52
+ /** HQ's setup worker folder and skills, resolved like `hq bot create --worker setup`. */
53
+ export declare function loadSetupWorkerSkills(hqRoot: string): {
54
+ workerDir: string;
55
+ skills: WorkerSkillSource[];
56
+ } | null;
57
+ /**
58
+ * The prompt sections for a pack bot, or null when the bot's worker is not a
59
+ * pack worker or its pack has no valid `setup:` block.
60
+ */
61
+ export declare function resolvePackSetupPrompt(input: ResolvePackSetupPromptInput): PackSetupPromptSections | null;
62
+ //# sourceMappingURL=pack-setup.d.ts.map
@@ -0,0 +1,103 @@
1
+ /**
2
+ * A bot whose worker belongs to an installed pack with a `setup:` block gets
3
+ * an auto-generated "Pack setup" section in its system prompt, and, when the
4
+ * pack asks for it and HQ's own setup is unfinished, a takeover section with
5
+ * HQ's setup worker skills (build contract section 3).
6
+ *
7
+ * scan-packages links a pack worker at `core/workers/public/<id>` ->
8
+ * `core/packages/<pack>/workers/<id>`, so the pack is found by resolving the
9
+ * worker folder's real path and checking whether it sits under
10
+ * `<hqRoot>/core/packages/<pack>/`.
11
+ */
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ import { readInstalledPackSetup } from "../packages/setup-block.js";
15
+ import { renderPackSetupSection } from "../packages/setup-prompt.js";
16
+ import { decideTakeover, readHqSetupProgressNote, renderTakeoverSection } from "../packages/setup-takeover.js";
17
+ import { readPackManifest } from "../../utils/pack-contributions.js";
18
+ import { readPromptSources } from "./prompt.js";
19
+ import { resolveBotWorker } from "./worker-source.js";
20
+ import { SETUP_BOT_WORKER_ID } from "./config.js";
21
+ const defaultFs = {
22
+ realpathSync: (p) => fs.realpathSync(p),
23
+ existsSync: (p) => fs.existsSync(p),
24
+ };
25
+ /** The installed pack (name + root) a worker folder belongs to, or null. */
26
+ export function findPackForWorker(hqRoot, workerDir, io = defaultFs) {
27
+ let packagesRoot;
28
+ let workerReal;
29
+ try {
30
+ packagesRoot = io.realpathSync(path.join(hqRoot, "core", "packages"));
31
+ workerReal = io.realpathSync(path.isAbsolute(workerDir) ? workerDir : path.join(hqRoot, workerDir));
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ const rel = path.relative(packagesRoot, workerReal);
37
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel))
38
+ return null;
39
+ const packName = rel.split(path.sep)[0];
40
+ if (!packName || packName.startsWith("."))
41
+ return null;
42
+ return { packName, packRoot: path.join(packagesRoot, packName) };
43
+ }
44
+ /** HQ's setup worker folder and skills, resolved like `hq bot create --worker setup`. */
45
+ export function loadSetupWorkerSkills(hqRoot) {
46
+ try {
47
+ const worker = resolveBotWorker(hqRoot, SETUP_BOT_WORKER_ID);
48
+ const sources = readPromptSources(hqRoot, worker.path, undefined, { workerSource: "worker" });
49
+ return { workerDir: worker.path, skills: sources.worker?.skills ?? [] };
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ /**
56
+ * The prompt sections for a pack bot, or null when the bot's worker is not a
57
+ * pack worker or its pack has no valid `setup:` block.
58
+ */
59
+ export function resolvePackSetupPrompt(input) {
60
+ if (input.workerSource !== "worker")
61
+ return null;
62
+ const log = input.log ?? (() => { });
63
+ const found = findPackForWorker(input.hqRoot, input.workerDir, input.io);
64
+ if (!found)
65
+ return null;
66
+ const { manifest, error } = readPackManifest(found.packRoot);
67
+ if (!manifest) {
68
+ log("warn", `worker ${input.workerDir} belongs to pack ${found.packName} but its package.yaml is unreadable (${error ?? "unknown"}); no pack setup section`);
69
+ return null;
70
+ }
71
+ if (manifest.setup === undefined)
72
+ return null;
73
+ const setup = readInstalledPackSetup(manifest, found.packRoot, input.io?.existsSync);
74
+ if (!setup) {
75
+ log("warn", `pack ${found.packName} has a setup: block that does not validate; no pack setup section`);
76
+ return null;
77
+ }
78
+ const promptInput = { packName: found.packName, packRoot: found.packRoot, setup };
79
+ const sections = {
80
+ packName: found.packName,
81
+ packRoot: found.packRoot,
82
+ setup,
83
+ packSetup: renderPackSetupSection(promptInput),
84
+ };
85
+ if (setup.takeover_setup) {
86
+ const progress = (input.readProgressNote ?? readHqSetupProgressNote)();
87
+ if (decideTakeover({ takeoverSetup: true, progressNote: progress.note })) {
88
+ const setupWorker = (input.setupWorkerSkills ?? loadSetupWorkerSkills)(input.hqRoot);
89
+ sections.takeover = renderTakeoverSection({
90
+ packName: found.packName,
91
+ setupWorkerDir: setupWorker?.workerDir ?? path.join("core", "workers", SETUP_BOT_WORKER_ID),
92
+ skills: setupWorker?.skills ?? [],
93
+ progressNotePath: progress.path,
94
+ });
95
+ log("info", `pack ${found.packName}: HQ setup is not finished; this bot finishes it before the pack setup`);
96
+ }
97
+ else {
98
+ log("info", `pack ${found.packName}: HQ setup is finished; going straight to the pack setup`);
99
+ }
100
+ }
101
+ return sections;
102
+ }
103
+ //# sourceMappingURL=pack-setup.js.map
@@ -93,6 +93,14 @@ export interface BuildSystemPromptInput {
93
93
  kind?: BotKind;
94
94
  /** Company bots: the slugs of the companies the bot is a member of. */
95
95
  companies?: string[];
96
+ /**
97
+ * Pack bot: the auto-generated "Pack setup" section (and the HQ setup
98
+ * takeover section when it applies), rendered by lib/bot/pack-setup.ts.
99
+ */
100
+ packSetup?: {
101
+ takeover?: string;
102
+ packSetup: string;
103
+ } | null;
96
104
  /** hqRoot-relative (default `${workerDir}/memory`) or absolute. */
97
105
  memoryDir?: string;
98
106
  }
@@ -373,6 +373,11 @@ export function buildSystemPrompt(input) {
373
373
  parts.push(`## Persona (${path.join(input.workerDir, "persona.md")})\n\n${input.sources.persona.trim()}`);
374
374
  }
375
375
  }
376
+ if (input.packSetup) {
377
+ if (input.packSetup.takeover)
378
+ parts.push(input.packSetup.takeover);
379
+ parts.push(input.packSetup.packSetup);
380
+ }
376
381
  if (input.sources.memory.length > 0) {
377
382
  const blocks = input.sources.memory.map((m) => `### ${m.name}\n\n${m.body.trim()}`);
378
383
  parts.push(`## Memory (${memoryDir}/)\n\n${blocks.join("\n\n")}`);
@@ -201,13 +201,25 @@ export declare const KICKOFF_MESSAGE_ID = "kickoff";
201
201
  export declare function turnEnv(base: NodeJS.ProcessEnv, kind: BotKind, agentUid?: string, companySlug?: string): NodeJS.ProcessEnv;
202
202
  /** The core worker HQ's setup bot runs. */
203
203
  export declare const SETUP_WORKER_ID = "setup";
204
+ /**
205
+ * The executable a bot's turns will spawn, for the start-up log: an absolute
206
+ * path when the runtime found one (the HQ app toolchain, a system prefix, the
207
+ * Claude desktop app), the bare name when it relies on PATH, or "not found"
208
+ * when the bare name is on no PATH entry either.
209
+ */
210
+ export declare function describeRuntimeBinary(runtime: BotRuntime, env: NodeJS.ProcessEnv, cwd: string): string;
204
211
  /** The one message a person gets when a turn on their message did not finish. */
205
212
  export declare function didNotFinishText(reason: string, progressPosted: boolean): string;
206
213
  export { containsNoReplySentinel };
207
214
  /** Grok emits is_error with this message when the model produces only reasoning tokens. */
208
215
  export declare function isReasoningOnlyError(excerpt: string): boolean;
209
- /** The one note a person gets when the bot pauses after repeated failures. */
210
- export declare function holdNoticeText(holdMs: number, messagePending: boolean): string;
216
+ /**
217
+ * The one note a person gets when the bot pauses after repeated failures. With
218
+ * `reason` (the plain failure text of the last turn) it is the summary that
219
+ * replaces the fifth per-turn line: one message for the run of failures, with
220
+ * what went wrong and the one next step.
221
+ */
222
+ export declare function holdNoticeText(holdMs: number, messagePending: boolean, reason?: string): string;
211
223
  /** Owner stop, `hq bot stop` or `hq bot restart`: all arrive as the same signal. */
212
224
  export declare const STOPPED_BY_OWNER_REASON = "I was stopped before I was done";
213
225
  export declare const RESTARTED_REASON = "I was restarted while I was working on it";
@@ -229,4 +241,8 @@ export interface BotHandle {
229
241
  export declare function inboxLaneKey(item: InboxItem, config: Pick<BotConfig, "ownerUid" | "agentUid" | "name">): string;
230
242
  export declare function preflightCreds(dir: string, config: BotConfig): string | null;
231
243
  export declare function runBot(deps: BotRunDeps): Promise<BotHandle>;
244
+ /** True for a setup note the desktop app sent, not words the person typed. */
245
+ export declare function isAppSetupNote(text: string): boolean;
246
+ /** Put ahead of an app setup note: fold it in, and stay quiet unless it changes what to say. */
247
+ export declare const APP_SETUP_NOTE_TURN_NOTE: string;
232
248
  //# sourceMappingURL=run.d.ts.map