@indigoai-us/hq-cli 5.235.0 → 5.237.0
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 +57 -0
- package/assets/bot-workers/setup/skills/agents-and-team.md +154 -0
- package/assets/bot-workers/setup/skills/first-company.md +4 -1
- package/assets/bot-workers/setup/skills/pricing.md +92 -0
- package/assets/bot-workers/setup/skills/standing-help.md +7 -0
- package/assets/bot-workers/setup/worker.yaml +244 -63
- package/dist/command-catalog.generated.d.ts +3 -0
- package/dist/command-catalog.generated.js +4 -0
- package/dist/commands/bot.d.ts +2 -0
- package/dist/commands/bot.js +14 -2
- package/dist/commands/lanes-checks.js +22 -1
- package/dist/lib/bot/config.d.ts +12 -0
- package/dist/lib/bot/config.js +25 -0
- package/dist/lib/bot/local-config.d.ts +2 -1
- package/dist/lib/bot/local-config.js +4 -0
- package/dist/lib/bot/progress.d.ts +25 -0
- package/dist/lib/bot/progress.js +39 -1
- package/dist/lib/bot/prompt.d.ts +2 -0
- package/dist/lib/bot/prompt.js +12 -2
- package/dist/lib/bot/run.js +10 -2
- package/dist/lib/lanes/store.d.ts +3 -0
- package/dist/lib/lanes/store.js +82 -3
- package/dist/lib/lanes/types.d.ts +4 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/node-network-compat.d.ts +1 -10
- package/dist/node-network-compat.js +3 -70
- package/package.json +1 -1
- package/dist/node-network-compat-flag.d.ts +0 -19
- package/dist/node-network-compat-flag.js +0 -87
package/dist/lib/bot/config.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export declare function normalizeBotCompanies(values: readonly string[] | undefi
|
|
|
29
29
|
export declare function effectiveBotKind(config: Pick<BotConfig, "kind" | "workerId">): BotKind;
|
|
30
30
|
/** The companies a bot belongs to (empty for a personal bot: its owner's memberships apply). */
|
|
31
31
|
export declare function effectiveBotCompanies(config: Pick<BotConfig, "kind" | "companies" | "workerId">): string[];
|
|
32
|
+
/** Longest display name (Slack's own limit for a bot's display name). */
|
|
33
|
+
export declare const BOT_DISPLAY_NAME_MAX_CHARS = 35;
|
|
32
34
|
/** Longest `intro` (first-start hello DM) a bot may store. */
|
|
33
35
|
export declare const BOT_INTRO_MAX_CHARS = 500;
|
|
34
36
|
/** Longest `kickoff` (first-start model turn prompt) a bot may store. */
|
|
@@ -61,6 +63,11 @@ export declare function validateBotEffort(runtime: BotRuntimeId, value: string):
|
|
|
61
63
|
* it cannot be a model id.
|
|
62
64
|
*/
|
|
63
65
|
export declare function validateBotModel(value: string): string | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Trim and check a display name: the human name people see in HQ (the DM list,
|
|
68
|
+
* the bot's own "You are …"). The handle (`name`) stays the CLI's key.
|
|
69
|
+
*/
|
|
70
|
+
export declare function validateBotDisplayName(text: string): string;
|
|
64
71
|
/** Trim and bound the intro text; throws an expected error when unusable. */
|
|
65
72
|
export declare function validateBotIntro(text: string): string;
|
|
66
73
|
/** Trim and bound the kickoff prompt; throws an expected error when unusable. */
|
|
@@ -112,6 +119,11 @@ export interface BotConfig {
|
|
|
112
119
|
* the synced tree (~/.hq/bots/<name>/memory).
|
|
113
120
|
*/
|
|
114
121
|
memoryDir?: string;
|
|
122
|
+
/**
|
|
123
|
+
* The human name people see (e.g. "Maya"). Sent to HQ as the bot's name at
|
|
124
|
+
* create; `name` stays the handle every hq bot command uses. Unset = `name`.
|
|
125
|
+
*/
|
|
126
|
+
displayName?: string;
|
|
115
127
|
/**
|
|
116
128
|
* Verbatim hello the bot DMs its owner when it first comes online (and on
|
|
117
129
|
* `hq bot intro`). Unset = the default introduction.
|
package/dist/lib/bot/config.js
CHANGED
|
@@ -64,6 +64,8 @@ export function effectiveBotCompanies(config) {
|
|
|
64
64
|
}
|
|
65
65
|
return listed;
|
|
66
66
|
}
|
|
67
|
+
/** Longest display name (Slack's own limit for a bot's display name). */
|
|
68
|
+
export const BOT_DISPLAY_NAME_MAX_CHARS = 35;
|
|
67
69
|
/** Longest `intro` (first-start hello DM) a bot may store. */
|
|
68
70
|
export const BOT_INTRO_MAX_CHARS = 500;
|
|
69
71
|
/** Longest `kickoff` (first-start model turn prompt) a bot may store. */
|
|
@@ -120,6 +122,29 @@ export function validateBotModel(value) {
|
|
|
120
122
|
}
|
|
121
123
|
return v;
|
|
122
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Trim and check a display name: the human name people see in HQ (the DM list,
|
|
127
|
+
* the bot's own "You are …"). The handle (`name`) stays the CLI's key.
|
|
128
|
+
*/
|
|
129
|
+
export function validateBotDisplayName(text) {
|
|
130
|
+
const trimmed = (text ?? "").replace(/\s+/g, " ").trim();
|
|
131
|
+
if (!trimmed) {
|
|
132
|
+
throw Object.assign(new Error("--display-name needs some text (the name people see, e.g. Maya)."), {
|
|
133
|
+
expected: true,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (trimmed.length > BOT_DISPLAY_NAME_MAX_CHARS) {
|
|
137
|
+
throw Object.assign(new Error(`--display-name is too long (${trimmed.length} chars); keep it under ${BOT_DISPLAY_NAME_MAX_CHARS}.`), {
|
|
138
|
+
expected: true,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (!/^[\p{L}\p{M}][\p{L}\p{M} .'-]*$/u.test(trimmed)) {
|
|
142
|
+
throw Object.assign(new Error("--display-name can use letters, spaces, apostrophes, periods and hyphens."), {
|
|
143
|
+
expected: true,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return trimmed;
|
|
147
|
+
}
|
|
123
148
|
/** Trim and bound the intro text; throws an expected error when unusable. */
|
|
124
149
|
export function validateBotIntro(text) {
|
|
125
150
|
const trimmed = (text ?? "").replace(/\r\n/g, "\n").trim();
|
|
@@ -34,13 +34,14 @@ export interface BotLocalConfig {
|
|
|
34
34
|
autoApprove?: boolean;
|
|
35
35
|
/** Only the memory *kind* travels — never an absolute path from another Mac. */
|
|
36
36
|
memory?: BotMemoryMode;
|
|
37
|
+
displayName?: string;
|
|
37
38
|
intro?: string;
|
|
38
39
|
kickoff?: string;
|
|
39
40
|
}
|
|
40
41
|
/** Walks a value and throws when any key looks like it could carry a secret. */
|
|
41
42
|
export declare function assertNoSecretKeys(value: unknown, path?: string): void;
|
|
42
43
|
/** The bot.json fields a localConfig is built from — nothing else is read. */
|
|
43
|
-
export type BotLocalConfigSource = Pick<BotConfig, "runtime" | "workerSource" | "workerId" | "companySlug" | "model" | "effort" | "kind" | "companies" | "autoApprove" | "memoryDir" | "intro" | "kickoff">;
|
|
44
|
+
export type BotLocalConfigSource = Pick<BotConfig, "runtime" | "workerSource" | "workerId" | "companySlug" | "model" | "effort" | "kind" | "companies" | "autoApprove" | "memoryDir" | "displayName" | "intro" | "kickoff">;
|
|
44
45
|
/**
|
|
45
46
|
* The non-secret settings of a bot, for the cloud to keep. Absolute paths
|
|
46
47
|
* (hqRoot, memoryDir, workerDir) are deliberately left out: they belong to the
|
|
@@ -51,6 +51,7 @@ export function buildBotLocalConfig(config) {
|
|
|
51
51
|
...(kind === "company" ? { companies } : {}),
|
|
52
52
|
...(config.autoApprove === false ? { autoApprove: false } : {}),
|
|
53
53
|
memory: botMemoryMode(config),
|
|
54
|
+
...(config.displayName ? { displayName: config.displayName } : {}),
|
|
54
55
|
...(config.intro ? { intro: config.intro } : {}),
|
|
55
56
|
...(config.kickoff ? { kickoff: config.kickoff } : {}),
|
|
56
57
|
};
|
|
@@ -107,6 +108,9 @@ export function parseBotLocalConfig(value) {
|
|
|
107
108
|
const memory = str("memory");
|
|
108
109
|
if (isBotMemoryMode(memory))
|
|
109
110
|
out.memory = memory;
|
|
111
|
+
const displayName = str("displayName");
|
|
112
|
+
if (displayName)
|
|
113
|
+
out.displayName = displayName;
|
|
110
114
|
const intro = str("intro");
|
|
111
115
|
if (intro)
|
|
112
116
|
out.intro = intro;
|
|
@@ -27,6 +27,9 @@ export declare const PROGRESS_COALESCE_MS = 2000;
|
|
|
27
27
|
export declare const PROGRESS_MIN_GAP_MS = 4000;
|
|
28
28
|
export declare const WORKING_NOTICE_MS = 60000;
|
|
29
29
|
export declare const WORKING_NOTICE_TEXT = "Working on it \u2014 I'll reply here when it's done.";
|
|
30
|
+
/** The setup bot's heads-up, sent once when a turn runs past SETUP_WORKING_NOTICE_MS. */
|
|
31
|
+
export declare const SETUP_WORKING_NOTICE_MS = 30000;
|
|
32
|
+
export declare const SETUP_WORKING_NOTICE_TEXT = "This one takes a minute. I'll reply here as soon as it's done.";
|
|
30
33
|
export interface ProgressPosterOptions {
|
|
31
34
|
/** Post one message; resolve true when it was delivered. */
|
|
32
35
|
send: (body: string) => Promise<boolean>;
|
|
@@ -49,6 +52,14 @@ export interface ProgressPosterOptions {
|
|
|
49
52
|
* (when the turn is not silent); `abandon` posts nothing.
|
|
50
53
|
*/
|
|
51
54
|
buffer?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Never post a message the model wrote right before a tool call. Those are
|
|
57
|
+
* the model's notes to itself ("Now write the worker."), not replies: the
|
|
58
|
+
* runtime stream shows them as a text block followed by a tool_use. A
|
|
59
|
+
* message is held until the next event says which it was: a tool call drops
|
|
60
|
+
* it, anything else (another message, the end of the turn) keeps it.
|
|
61
|
+
*/
|
|
62
|
+
dropPreToolMessages?: boolean;
|
|
52
63
|
}
|
|
53
64
|
/** Whitespace-insensitive form used to recognise repeated text. */
|
|
54
65
|
export declare function normalizeForCompare(text: string): string;
|
|
@@ -65,6 +76,9 @@ export declare class ProgressPoster {
|
|
|
65
76
|
private readonly workingNoticeMs;
|
|
66
77
|
private readonly workingNoticeText;
|
|
67
78
|
private readonly buffer;
|
|
79
|
+
private readonly dropPreTool;
|
|
80
|
+
/** dropPreToolMessages: messages waiting to learn whether a tool call follows. */
|
|
81
|
+
private candidates;
|
|
68
82
|
private pending;
|
|
69
83
|
/** In-between questions, held until the turn ends (see the header). */
|
|
70
84
|
private heldQuestions;
|
|
@@ -77,11 +91,22 @@ export declare class ProgressPoster {
|
|
|
77
91
|
private finished;
|
|
78
92
|
private noticeSent;
|
|
79
93
|
private delivered;
|
|
94
|
+
/**
|
|
95
|
+
* Posts handed to the send chain this turn, counted when queued rather than
|
|
96
|
+
* when delivered. The "(no reply)" fallback checks this: a post still in
|
|
97
|
+
* flight when the turn ends has not bumped `delivered` yet, and counting only
|
|
98
|
+
* deliveries posted the placeholder right after a real message.
|
|
99
|
+
*/
|
|
100
|
+
private queued;
|
|
80
101
|
constructor(opts: ProgressPosterOptions);
|
|
81
102
|
/** Arm the one-time "working on it" note. Call when the turn starts. */
|
|
82
103
|
start(): void;
|
|
83
104
|
/** One finished assistant message from the model. */
|
|
84
105
|
message(text: string): void;
|
|
106
|
+
/** The model called a tool: whatever it wrote just before was a note to itself. */
|
|
107
|
+
tool(): void;
|
|
108
|
+
private promoteCandidates;
|
|
109
|
+
private accept;
|
|
85
110
|
/** Posts that actually reached the chat (progress, notice and final). */
|
|
86
111
|
get postedCount(): number;
|
|
87
112
|
/** True once any message from the model was said this turn (not counting the notice). */
|
package/dist/lib/bot/progress.js
CHANGED
|
@@ -27,6 +27,9 @@ export const PROGRESS_COALESCE_MS = 2_000;
|
|
|
27
27
|
export const PROGRESS_MIN_GAP_MS = 4_000;
|
|
28
28
|
export const WORKING_NOTICE_MS = 60_000;
|
|
29
29
|
export const WORKING_NOTICE_TEXT = "Working on it — I'll reply here when it's done.";
|
|
30
|
+
/** The setup bot's heads-up, sent once when a turn runs past SETUP_WORKING_NOTICE_MS. */
|
|
31
|
+
export const SETUP_WORKING_NOTICE_MS = 30_000;
|
|
32
|
+
export const SETUP_WORKING_NOTICE_TEXT = "This one takes a minute. I'll reply here as soon as it's done.";
|
|
30
33
|
/** Whitespace-insensitive form used to recognise repeated text. */
|
|
31
34
|
export function normalizeForCompare(text) {
|
|
32
35
|
return text.replace(/\s+/g, "");
|
|
@@ -46,6 +49,9 @@ export class ProgressPoster {
|
|
|
46
49
|
workingNoticeMs;
|
|
47
50
|
workingNoticeText;
|
|
48
51
|
buffer;
|
|
52
|
+
dropPreTool;
|
|
53
|
+
/** dropPreToolMessages: messages waiting to learn whether a tool call follows. */
|
|
54
|
+
candidates = [];
|
|
49
55
|
pending = [];
|
|
50
56
|
/** In-between questions, held until the turn ends (see the header). */
|
|
51
57
|
heldQuestions = [];
|
|
@@ -58,6 +64,13 @@ export class ProgressPoster {
|
|
|
58
64
|
finished = false;
|
|
59
65
|
noticeSent = false;
|
|
60
66
|
delivered = 0;
|
|
67
|
+
/**
|
|
68
|
+
* Posts handed to the send chain this turn, counted when queued rather than
|
|
69
|
+
* when delivered. The "(no reply)" fallback checks this: a post still in
|
|
70
|
+
* flight when the turn ends has not bumped `delivered` yet, and counting only
|
|
71
|
+
* deliveries posted the placeholder right after a real message.
|
|
72
|
+
*/
|
|
73
|
+
queued = 0;
|
|
61
74
|
constructor(opts) {
|
|
62
75
|
this.send = opts.send;
|
|
63
76
|
this.sendFinalFn = opts.sendFinal;
|
|
@@ -69,6 +82,7 @@ export class ProgressPoster {
|
|
|
69
82
|
this.workingNoticeMs = opts.workingNoticeMs ?? WORKING_NOTICE_MS;
|
|
70
83
|
this.workingNoticeText = opts.workingNoticeText ?? WORKING_NOTICE_TEXT;
|
|
71
84
|
this.buffer = opts.buffer ?? false;
|
|
85
|
+
this.dropPreTool = opts.dropPreToolMessages ?? false;
|
|
72
86
|
}
|
|
73
87
|
/** Arm the one-time "working on it" note. Call when the turn starts. */
|
|
74
88
|
start() {
|
|
@@ -93,6 +107,27 @@ export class ProgressPoster {
|
|
|
93
107
|
// silence signal, not content, and must never be posted as progress.
|
|
94
108
|
if (containsNoReplySentinel(t))
|
|
95
109
|
return;
|
|
110
|
+
if (this.dropPreTool) {
|
|
111
|
+
// The previous message was not followed by a tool call: it was real.
|
|
112
|
+
this.promoteCandidates();
|
|
113
|
+
this.candidates.push(t);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.accept(t);
|
|
117
|
+
}
|
|
118
|
+
/** The model called a tool: whatever it wrote just before was a note to itself. */
|
|
119
|
+
tool() {
|
|
120
|
+
if (this.finished || !this.dropPreTool)
|
|
121
|
+
return;
|
|
122
|
+
this.candidates = [];
|
|
123
|
+
}
|
|
124
|
+
promoteCandidates() {
|
|
125
|
+
const kept = this.candidates;
|
|
126
|
+
this.candidates = [];
|
|
127
|
+
for (const t of kept)
|
|
128
|
+
this.accept(t);
|
|
129
|
+
}
|
|
130
|
+
accept(t) {
|
|
96
131
|
this.cancelNotice();
|
|
97
132
|
this.said.push(t);
|
|
98
133
|
// An unmentioned room turn holds every in-between message: nothing is posted
|
|
@@ -124,6 +159,8 @@ export class ProgressPoster {
|
|
|
124
159
|
async finish(finalText) {
|
|
125
160
|
if (this.finished)
|
|
126
161
|
return this.chain;
|
|
162
|
+
// A message the turn ended on (no tool after it) is real.
|
|
163
|
+
this.promoteCandidates();
|
|
127
164
|
this.finished = true;
|
|
128
165
|
this.cancelNotice();
|
|
129
166
|
this.cancelFlush();
|
|
@@ -161,7 +198,7 @@ export class ProgressPoster {
|
|
|
161
198
|
const shouldAddFinal = Boolean(final && !this.repeatsSaid(final));
|
|
162
199
|
if (!shouldAddFinal && held.length > 0)
|
|
163
200
|
this.pending.push(...held);
|
|
164
|
-
if (this.pending.length === 0 && this.
|
|
201
|
+
if (this.pending.length === 0 && this.queued === 0 && !shouldAddFinal)
|
|
165
202
|
this.pending.push("(no reply)");
|
|
166
203
|
if (this.sendFinalFn && shouldAddFinal) {
|
|
167
204
|
// Flush any remaining progress messages first, then the final reply separately.
|
|
@@ -229,6 +266,7 @@ export class ProgressPoster {
|
|
|
229
266
|
// path built this body.
|
|
230
267
|
if (containsNoReplySentinel(body))
|
|
231
268
|
return;
|
|
269
|
+
this.queued += 1;
|
|
232
270
|
this.lastPostAt = this.now();
|
|
233
271
|
this.chain = this.chain.then(async () => {
|
|
234
272
|
try {
|
package/dist/lib/bot/prompt.d.ts
CHANGED
|
@@ -77,6 +77,8 @@ export declare function normalizeInstructions(raw: unknown): string | undefined;
|
|
|
77
77
|
export declare function readPromptSources(hqRoot: string, workerDir: string, io?: PromptFs, opts?: ReadPromptSourcesOptions): PromptSources;
|
|
78
78
|
export interface BuildSystemPromptInput {
|
|
79
79
|
botName: string;
|
|
80
|
+
/** The human name people call the bot (e.g. "Maya"); unset = botName. */
|
|
81
|
+
displayName?: string;
|
|
80
82
|
agentUid: string;
|
|
81
83
|
ownerUid: string;
|
|
82
84
|
hqRoot: string;
|
package/dist/lib/bot/prompt.js
CHANGED
|
@@ -229,6 +229,16 @@ export function readPromptSources(hqRoot, workerDir, io = defaultPromptFs, opts
|
|
|
229
229
|
}
|
|
230
230
|
return { workerYaml, persona: read("persona.md"), memory };
|
|
231
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* How the bot names itself: its display name when it has one ("Maya", with the
|
|
234
|
+
* handle kept for commands), else its handle.
|
|
235
|
+
*/
|
|
236
|
+
function introName(input) {
|
|
237
|
+
const display = input.displayName?.trim();
|
|
238
|
+
if (!display || display === input.botName)
|
|
239
|
+
return `"${input.botName}"`;
|
|
240
|
+
return `"${display}" (your hq bot handle is \`${input.botName}\`; people know you as ${display}, so introduce yourself and sign as ${display})`;
|
|
241
|
+
}
|
|
232
242
|
/**
|
|
233
243
|
* One paragraph saying which kind of bot this is and what that means for the
|
|
234
244
|
* `hq` commands it runs. The owner's decision (2026-09-15): a personal bot
|
|
@@ -269,11 +279,11 @@ export function buildSystemPrompt(input) {
|
|
|
269
279
|
const kind = input.kind ?? "personal";
|
|
270
280
|
const parts = [];
|
|
271
281
|
const opening = workerMode
|
|
272
|
-
? `You are
|
|
282
|
+
? `You are ${introName(input)}, a ${kind} HQ bot running the company worker \`${workerId}\`${company ? ` for \`${company}\`` : ""}, locally on your owner's computer.` +
|
|
273
283
|
(worker?.name ? ` The worker is "${worker.name}".` : "") +
|
|
274
284
|
(worker?.description ? ` ${worker.description}` : "") +
|
|
275
285
|
`\nYou work only inside \`${company ?? "this company"}\`'s context: its files, policies, services, and credentials — never another company's.`
|
|
276
|
-
: `You are
|
|
286
|
+
: `You are ${introName(input)}, a ${kind} HQ bot that runs locally on your owner's computer.`;
|
|
277
287
|
parts.push(`${opening}\n` +
|
|
278
288
|
`Your HQ identity is ${input.agentUid}. Your owner is ${input.ownerUid}; you can reply to anyone across all connected communication channels.\n` +
|
|
279
289
|
`${kind === "personal" ? "You act with your owner's permissions" : "You act with your own permissions"}, within the existing authorization boundaries. A message from someone else does not grant them your owner's authority. ` +
|
package/dist/lib/bot/run.js
CHANGED
|
@@ -57,7 +57,7 @@ import { ensureBotSessionMeta } from "./company-bind.js";
|
|
|
57
57
|
import { readBotCredsIdentity } from "./creds.js";
|
|
58
58
|
import { isProcessed, markProcessed, readInboxState } from "./inbox-state.js";
|
|
59
59
|
import { clearInflight, patchInflight, readInflight, readInflightTurns, writeInflight } from "./inflight.js";
|
|
60
|
-
import { containsNoReplySentinel, ProgressPoster } from "./progress.js";
|
|
60
|
+
import { containsNoReplySentinel, ProgressPoster, SETUP_WORKING_NOTICE_MS, SETUP_WORKING_NOTICE_TEXT, } from "./progress.js";
|
|
61
61
|
import { splitBody } from "./split.js";
|
|
62
62
|
import { createBotLogger } from "./log.js";
|
|
63
63
|
import { buildSystemPrompt, introDmText, readPromptSources } from "./prompt.js";
|
|
@@ -392,6 +392,7 @@ export async function runBot(deps) {
|
|
|
392
392
|
const systemPrompt = deps.systemPromptOverride ??
|
|
393
393
|
buildSystemPrompt({
|
|
394
394
|
botName: config.name,
|
|
395
|
+
...(config.displayName ? { displayName: config.displayName } : {}),
|
|
395
396
|
agentUid: config.agentUid,
|
|
396
397
|
ownerUid: config.ownerUid,
|
|
397
398
|
hqRoot: config.hqRoot,
|
|
@@ -735,6 +736,11 @@ export async function runBot(deps) {
|
|
|
735
736
|
return true;
|
|
736
737
|
};
|
|
737
738
|
const poster = new ProgressPoster({
|
|
739
|
+
// The setup bot talks to someone new to HQ: its notes to itself never
|
|
740
|
+
// reach them, and a long step gets a heads-up instead of silence.
|
|
741
|
+
...(isSetupBot
|
|
742
|
+
? { dropPreToolMessages: true, workingNoticeMs: SETUP_WORKING_NOTICE_MS, workingNoticeText: SETUP_WORKING_NOTICE_TEXT }
|
|
743
|
+
: {}),
|
|
738
744
|
...deps.progress,
|
|
739
745
|
...(args.workingNotice === false || silenceable ? { workingNoticeMs: 0 } : {}),
|
|
740
746
|
...(silenceable ? { buffer: true } : {}),
|
|
@@ -749,6 +755,8 @@ export async function runBot(deps) {
|
|
|
749
755
|
onEvent: (event) => {
|
|
750
756
|
if (event.kind === "message")
|
|
751
757
|
poster.message(event.text);
|
|
758
|
+
else if (event.kind === "tool")
|
|
759
|
+
poster.tool();
|
|
752
760
|
},
|
|
753
761
|
signal: controller.signal,
|
|
754
762
|
...(args.readDirs?.length ? { readDirs: args.readDirs } : {}),
|
|
@@ -1450,7 +1458,7 @@ export async function runBot(deps) {
|
|
|
1450
1458
|
tReceived,
|
|
1451
1459
|
send,
|
|
1452
1460
|
sendFinal,
|
|
1453
|
-
|
|
1461
|
+
// The setup bot's own notice (SETUP_WORKING_NOTICE_TEXT) is set where the poster is built.
|
|
1454
1462
|
silenceable: target.kind === "room" && !target.mentioned,
|
|
1455
1463
|
...(readDirs.length ? { readDirs } : {}),
|
|
1456
1464
|
...(target.kind === "room"
|
|
@@ -52,6 +52,9 @@ export declare function tryReadLane(hqRoot: string, laneId: string, deps?: Store
|
|
|
52
52
|
export declare function readLane(hqRoot: string, laneId: string, deps?: StoreDeps): LaneRecord;
|
|
53
53
|
export declare function tryReadQuestion(hqRoot: string, questionId: string, deps?: StoreDeps): QuestionRecord | null;
|
|
54
54
|
export declare function readQuestion(hqRoot: string, questionId: string, deps?: StoreDeps): QuestionRecord;
|
|
55
|
+
/** Semver precedence includes prerelease identifiers (for example, rc.2 > rc.1). */
|
|
56
|
+
export declare function isLaneStoreVersionNewer(storedVersion: string, runningVersion: string): boolean;
|
|
57
|
+
export declare const LANE_STORE_UPGRADE_COMMAND = "npm install -g @indigoai-us/hq-cli@latest";
|
|
55
58
|
export declare function createLane(hqRoot: string, init: Omit<LaneRecord, "schema_version" | "lane_id" | "version" | "created_at" | "updated_at"> & Partial<Pick<LaneRecord, "schema_version" | "lane_id" | "version" | "created_at" | "updated_at">>, deps?: StoreDeps): LaneRecord;
|
|
56
59
|
/** Hierarchy lock: separate from record writes so callers can CAS-write while holding it. */
|
|
57
60
|
export declare function withLaneHireLocks<T>(hqRoot: string, laneIds: readonly string[], fn: () => T, deps?: StoreDeps): T;
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -10,6 +10,8 @@ import { randomBytes } from "node:crypto";
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as os from "node:os";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import semver from "semver";
|
|
14
|
+
import { CLI_VERSION } from "../../cli-version.js";
|
|
13
15
|
import { generateUlid } from "../mesh/live/ulid.js";
|
|
14
16
|
import { resolveLanesHqRoot } from "./hq-root.js";
|
|
15
17
|
import { LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesError, isLaneVersionConflict, } from "./errors.js";
|
|
@@ -391,6 +393,26 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
391
393
|
const schema_version = requireString(raw.schema_version, "schema_version");
|
|
392
394
|
parseMajorSchema(schema_version, "hq-lane", 1);
|
|
393
395
|
requireString(raw.lane_id, "lane_id");
|
|
396
|
+
if (raw.written_by_version !== undefined) {
|
|
397
|
+
const writtenByVersion = requireString(raw.written_by_version, "written_by_version");
|
|
398
|
+
if (!semver.valid(writtenByVersion)) {
|
|
399
|
+
throw new LaneSchemaError("written_by_version must be a valid semver version", {
|
|
400
|
+
path: filePath,
|
|
401
|
+
field: "written_by_version",
|
|
402
|
+
value: writtenByVersion,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (raw.owner_written_by_version !== undefined) {
|
|
407
|
+
const ownerWrittenByVersion = requireString(raw.owner_written_by_version, "owner_written_by_version");
|
|
408
|
+
if (!semver.valid(ownerWrittenByVersion)) {
|
|
409
|
+
throw new LaneSchemaError("owner_written_by_version must be a valid semver version", {
|
|
410
|
+
path: filePath,
|
|
411
|
+
field: "owner_written_by_version",
|
|
412
|
+
value: ownerWrittenByVersion,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
394
416
|
requireInt(raw.version, "version");
|
|
395
417
|
requireInt(raw.depth, "depth");
|
|
396
418
|
requireInt(raw.priority, "priority");
|
|
@@ -950,6 +972,52 @@ export function readQuestion(hqRoot, questionId, deps = defaultStoreDeps()) {
|
|
|
950
972
|
throw new LaneNotFoundError("question", questionId);
|
|
951
973
|
return record;
|
|
952
974
|
}
|
|
975
|
+
/** Semver precedence includes prerelease identifiers (for example, rc.2 > rc.1). */
|
|
976
|
+
export function isLaneStoreVersionNewer(storedVersion, runningVersion) {
|
|
977
|
+
if (!semver.valid(storedVersion)) {
|
|
978
|
+
throw new LanesError("lane_store_version_invalid", "Lane store writer version is not valid semver", {
|
|
979
|
+
stored_version: storedVersion,
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
if (!semver.valid(runningVersion)) {
|
|
983
|
+
throw new LanesError("lane_store_version_invalid", "Running hq-cli version is not valid semver", {
|
|
984
|
+
running_version: runningVersion,
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
return semver.gt(storedVersion, runningVersion);
|
|
988
|
+
}
|
|
989
|
+
const OLDER_LANE_STORE_WRITE_OVERRIDE = "HQ_LANES_ALLOW_OLDER_WRITE";
|
|
990
|
+
export const LANE_STORE_UPGRADE_COMMAND = "npm install -g @indigoai-us/hq-cli@latest";
|
|
991
|
+
const warnedBackgroundWriterLaneIds = new Set();
|
|
992
|
+
function assertLaneStoreWriteAllowed(lane, laneId, writer) {
|
|
993
|
+
const storedVersion = lane.written_by_version;
|
|
994
|
+
if (storedVersion === undefined || !isLaneStoreVersionNewer(storedVersion, CLI_VERSION))
|
|
995
|
+
return;
|
|
996
|
+
const warning = `Lane ${laneId} was last written by hq-cli ${storedVersion}, newer than running hq-cli ${CLI_VERSION}. Upgrade with \`${LANE_STORE_UPGRADE_COMMAND}\` before writing.`;
|
|
997
|
+
if (writer === "command" || writer === "creator") {
|
|
998
|
+
if (process.env[OLDER_LANE_STORE_WRITE_OVERRIDE] === "1") {
|
|
999
|
+
console.warn(`${warning} Proceeding because ${OLDER_LANE_STORE_WRITE_OVERRIDE}=1 was set for this command.`);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
throw new LanesError("lane_store_newer_cli", `${warning} For a one-command override, set ${OLDER_LANE_STORE_WRITE_OVERRIDE}=1.`, {
|
|
1003
|
+
lane_id: laneId,
|
|
1004
|
+
stored_version: storedVersion,
|
|
1005
|
+
running_version: CLI_VERSION,
|
|
1006
|
+
upgrade_command: LANE_STORE_UPGRADE_COMMAND,
|
|
1007
|
+
override: `${OLDER_LANE_STORE_WRITE_OVERRIDE}=1`,
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
if (!warnedBackgroundWriterLaneIds.has(laneId)) {
|
|
1011
|
+
warnedBackgroundWriterLaneIds.add(laneId);
|
|
1012
|
+
console.warn(`${warning} Continuing background ${writer} writes and preserving the newer stamp.`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
function newestLaneStoreVersion(storedVersion) {
|
|
1016
|
+
if (storedVersion !== undefined && isLaneStoreVersionNewer(storedVersion, CLI_VERSION)) {
|
|
1017
|
+
return storedVersion;
|
|
1018
|
+
}
|
|
1019
|
+
return CLI_VERSION;
|
|
1020
|
+
}
|
|
953
1021
|
function recordsEquivalentAsideFromClock(current, updated) {
|
|
954
1022
|
if (updated === current)
|
|
955
1023
|
return true;
|
|
@@ -961,7 +1029,7 @@ function recordsEquivalentAsideFromClock(current, updated) {
|
|
|
961
1029
|
};
|
|
962
1030
|
return JSON.stringify(strip(current)) === JSON.stringify(strip(updated));
|
|
963
1031
|
}
|
|
964
|
-
function casWrite(filePath, expectedVersion, parse, merge, updater, deps) {
|
|
1032
|
+
function casWrite(filePath, expectedVersion, parse, merge, updater, deps, options = {}) {
|
|
965
1033
|
return withRecordLock(filePath, deps, () => {
|
|
966
1034
|
const current = readMergedRecord(filePath, parse, merge, deps);
|
|
967
1035
|
if (current === null) {
|
|
@@ -980,9 +1048,11 @@ function casWrite(filePath, expectedVersion, parse, merge, updater, deps) {
|
|
|
980
1048
|
if (recordsEquivalentAsideFromClock(current, updated)) {
|
|
981
1049
|
return current;
|
|
982
1050
|
}
|
|
1051
|
+
options.beforeWrite?.(current);
|
|
983
1052
|
const next = {
|
|
984
1053
|
...current,
|
|
985
1054
|
...updated,
|
|
1055
|
+
...options.stamp?.(current),
|
|
986
1056
|
version: current.version + 1,
|
|
987
1057
|
updated_at: deps.now().toISOString(),
|
|
988
1058
|
};
|
|
@@ -996,6 +1066,7 @@ export function createLane(hqRoot, init, deps = defaultStoreDeps()) {
|
|
|
996
1066
|
const filePath = laneRecordPath(hqRoot, lane_id);
|
|
997
1067
|
const record = {
|
|
998
1068
|
...init,
|
|
1069
|
+
written_by_version: CLI_VERSION,
|
|
999
1070
|
hire_grant: init.hire_grant ?? 0,
|
|
1000
1071
|
schema_version: init.schema_version ?? LANE_SCHEMA_VERSION,
|
|
1001
1072
|
lane_id,
|
|
@@ -1060,7 +1131,10 @@ export function updateLane(hqRoot, laneId, expectedVersion, writer, updater, dep
|
|
|
1060
1131
|
}
|
|
1061
1132
|
}
|
|
1062
1133
|
return updated;
|
|
1063
|
-
}, deps
|
|
1134
|
+
}, deps, {
|
|
1135
|
+
beforeWrite: (current) => assertLaneStoreWriteAllowed(current, laneId, writer),
|
|
1136
|
+
stamp: (current) => ({ written_by_version: newestLaneStoreVersion(current.written_by_version) }),
|
|
1137
|
+
});
|
|
1064
1138
|
if (laneStoryLedgerEnabled() && before && laneLedgerRelevantChange(before, updated)) {
|
|
1065
1139
|
return syncStoryLedgerForLane(hqRoot, updated, deps);
|
|
1066
1140
|
}
|
|
@@ -1182,7 +1256,12 @@ export function syncLaneRecordFromOwner(hqRoot, laneId, outpostId, rawOwnerLane,
|
|
|
1182
1256
|
current.outpost_command_id !== undefined
|
|
1183
1257
|
? { ...ownerLane, outpost_command_id: current.outpost_command_id }
|
|
1184
1258
|
: ownerLane;
|
|
1185
|
-
const
|
|
1259
|
+
const merged = mergeLaneRecords(ownerSnapshot, [current]);
|
|
1260
|
+
const synced = {
|
|
1261
|
+
...merged,
|
|
1262
|
+
written_by_version: newestLaneStoreVersion(current.written_by_version),
|
|
1263
|
+
owner_written_by_version: ownerLane.written_by_version,
|
|
1264
|
+
};
|
|
1186
1265
|
parseLaneRecord(synced, filePath);
|
|
1187
1266
|
atomicWriteJson(filePath, synced, deps);
|
|
1188
1267
|
return synced;
|
|
@@ -318,6 +318,10 @@ export interface LaneStory {
|
|
|
318
318
|
export interface LaneRecord {
|
|
319
319
|
schema_version: typeof LANE_SCHEMA_VERSION | string;
|
|
320
320
|
lane_id: string;
|
|
321
|
+
/** CLI release that most recently wrote this lane record. Absent on legacy rows. */
|
|
322
|
+
written_by_version?: string;
|
|
323
|
+
/** CLI release that most recently wrote the owner-host copy synced to primary. */
|
|
324
|
+
owner_written_by_version?: string;
|
|
321
325
|
/** `null` is a core/personal lane (spec §6.2 / §10). Not an empty slug. */
|
|
322
326
|
company: LaneCompanyRef | null;
|
|
323
327
|
/** Explicit Work Mesh attribution for a personal lane; never inferred from cwd. */
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -216,7 +216,7 @@ export const REQUIRED_LANE_RECORD_FIELDS = [
|
|
|
216
216
|
export const LANE_WRITERS = ["creator", "command", "watcher", "reconcile", "spawn", "story-ledger"];
|
|
217
217
|
/** Field ownership at the durable-record mutation boundary (spec §10.1). */
|
|
218
218
|
export const LANE_FIELD_WRITERS = {
|
|
219
|
-
schema_version: ["creator"], lane_id: ["creator"], company: ["creator"],
|
|
219
|
+
schema_version: ["creator"], lane_id: ["creator"], written_by_version: [], owner_written_by_version: [], company: ["creator"],
|
|
220
220
|
project_id: ["command"], story_id: ["command"], no_story_reason: ["creator", "command"], ledger_sync_error: ["story-ledger"], pending_question_ids: ["story-ledger"], stories: ["creator", "command", "watcher", "reconcile", "spawn"], thread_id: ["creator", "command", "spawn"], mesh_company: ["creator", "command"],
|
|
221
221
|
depth: ["command"], senior: ["command"], senior_history: ["command"],
|
|
222
222
|
hire_grant: ["creator", "command"],
|
|
@@ -1,28 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Node network defaults for HQ CLI entry points.
|
|
3
3
|
*
|
|
4
|
-
* The compatibility timeout remains the default
|
|
5
|
-
* advertised but unusable, the default-off hq-flags gate can also prefer IPv4
|
|
6
|
-
* and disable automatic family racing before the CLI issues its first request.
|
|
4
|
+
* The compatibility timeout remains the default for HQ CLI entry points.
|
|
7
5
|
*
|
|
8
6
|
* Keep this module safe to import immediately after the Node version preflight.
|
|
9
|
-
* The hq-flags reader loads dynamically so an unsupported runtime exits through
|
|
10
|
-
* the preflight rather than failing during module linking.
|
|
11
7
|
*/
|
|
12
8
|
export declare const HQ_NETWORK_FAMILY_ATTEMPT_TIMEOUT_MS = 1000;
|
|
13
9
|
export interface NodeNetworkCompatibilityOptions {
|
|
14
10
|
nodeOptions?: string;
|
|
15
11
|
execArgv?: readonly string[];
|
|
16
|
-
forceIpv4?: boolean;
|
|
17
12
|
/**
|
|
18
13
|
* Inject the net timeout setter (tests). Pass null to simulate a runtime
|
|
19
14
|
* that lacks setDefaultAutoSelectFamilyAttemptTimeout.
|
|
20
15
|
*/
|
|
21
16
|
setAttemptTimeout?: ((milliseconds: number) => void) | null;
|
|
22
|
-
/** Inject Node's automatic family selection setter (tests). */
|
|
23
|
-
setAutoSelectFamily?: ((enabled: boolean) => void) | null;
|
|
24
|
-
/** Inject Node's DNS result-order setter (tests). */
|
|
25
|
-
setDefaultResultOrder?: ((order: "ipv4first") => void) | null;
|
|
26
17
|
}
|
|
27
18
|
export declare function configureNodeNetworkCompatibility(options?: NodeNetworkCompatibilityOptions): boolean;
|
|
28
19
|
//# sourceMappingURL=node-network-compat.d.ts.map
|