@indigoai-us/hq-cli 5.287.0 → 5.289.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 +48 -5
- package/assets/bot-workers/setup/skills/explain-hq.md +35 -0
- package/assets/bot-workers/setup/worker.yaml +25 -23
- package/dist/command-catalog.generated.d.ts +3 -0
- package/dist/command-catalog.generated.js +4 -0
- package/dist/commands/agent-enroll.js +10 -2
- package/dist/commands/agents.d.ts +1 -0
- package/dist/commands/agents.js +12 -4
- package/dist/commands/core-ontology.d.ts +4 -1
- package/dist/commands/core-ontology.js +26 -0
- package/dist/commands/lanes-resume.js +41 -2
- package/dist/commands/lanes-shared.d.ts +2 -0
- package/dist/commands/lanes.js +204 -6
- package/dist/lanes-drain-hook.d.ts +12 -0
- package/dist/lanes-drain-hook.js +59 -55
- package/dist/lib/bot/run.js +92 -2
- package/dist/lib/bot/setup-explain.d.ts +68 -0
- package/dist/lib/bot/setup-explain.js +141 -0
- package/dist/lib/checkpoint-ontology/merge-copies.d.ts +10 -1
- package/dist/lib/checkpoint-ontology/merge-copies.js +17 -2
- package/dist/lib/checkpoint-ontology/plan.d.ts +0 -88
- package/dist/lib/checkpoint-ontology/plan.js +80 -107
- package/dist/lib/lanes/dropbox.d.ts +1 -2
- package/dist/lib/lanes/dropbox.js +10 -111
- package/dist/lib/lanes/message-replay.d.ts +8 -0
- package/dist/lib/lanes/message-replay.js +119 -0
- package/dist/lib/lanes/store.js +30 -12
- package/dist/lib/lanes/types.d.ts +6 -2
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/plan-limit-nag.js +11 -0
- package/dist/main.js +17 -4
- package/dist/utils/cli-telemetry.d.ts +21 -0
- package/dist/utils/cli-telemetry.js +101 -1
- package/dist/utils/cognito-refresh-sentry.d.ts +3 -0
- package/dist/utils/cognito-refresh-sentry.js +49 -0
- package/dist/utils/cognito-session.js +55 -0
- package/package.json +1 -1
package/dist/lib/bot/run.js
CHANGED
|
@@ -52,7 +52,7 @@ import * as path from "node:path";
|
|
|
52
52
|
import { fullJitterDelayMs } from "../mesh/live/backoff.js";
|
|
53
53
|
import { acquirePidLock, defaultPidLockDeps, releasePidLock } from "../mesh/live/daemon/pid-lock.js";
|
|
54
54
|
import { BotApiError } from "./api.js";
|
|
55
|
-
import { effectiveBotCompanies, effectiveBotKind, patchBotConfig, readBotConfig, SETUP_BOT_WORKER_ID } from "./config.js";
|
|
55
|
+
import { effectiveBotCompanies, effectiveBotKind, patchBotConfig, readBotConfig, resolveBotMemoryDir, SETUP_BOT_WORKER_ID } from "./config.js";
|
|
56
56
|
import { ensureBotSessionMeta } from "./company-bind.js";
|
|
57
57
|
import { readBotCredsIdentity } from "./creds.js";
|
|
58
58
|
import { isProcessed, markProcessed, readInboxState } from "./inbox-state.js";
|
|
@@ -61,6 +61,7 @@ import { containsNoReplySentinel, ProgressPoster, } 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";
|
|
64
|
+
import { loadSetupExplainer, patchSetupIntroState, readSetupIntroState, readSetupProgressIntro, replyOffersExplain, SETUP_EXPLAINED_NOTE, setupExplainDue, } from "./setup-explain.js";
|
|
64
65
|
import { FALLBACK_CLAUDE_PERMISSION_MODE, isClaudeBypassDisabled } from "./runtime/claude.js";
|
|
65
66
|
import { runRuntimeTurn, RuntimeError } from "./runtime/index.js";
|
|
66
67
|
import { botAttachmentsDir, dmSessionScope } from "./paths.js";
|
|
@@ -296,6 +297,26 @@ export async function runBot(deps) {
|
|
|
296
297
|
const turnCwd = config.hqRoot;
|
|
297
298
|
// HQ's setup bot is a guided conversation: no "working on it" filler between steps.
|
|
298
299
|
const isSetupBot = config.workerId === SETUP_WORKER_ID;
|
|
300
|
+
/**
|
|
301
|
+
* Setup bot, owner DM: remember whether the bot's latest reply asked the
|
|
302
|
+
* explain-or-jump question, so a click on "Explain HQ first" can be answered
|
|
303
|
+
* at once (setup-explain.ts). Never lets a bookkeeping error touch the turn.
|
|
304
|
+
*/
|
|
305
|
+
const noteSetupReply = (reply) => {
|
|
306
|
+
if (!isSetupBot)
|
|
307
|
+
return;
|
|
308
|
+
try {
|
|
309
|
+
const offered = replyOffersExplain(reply);
|
|
310
|
+
const current = readSetupIntroState(dir);
|
|
311
|
+
if (offered && !current.offeredAt)
|
|
312
|
+
patchSetupIntroState(dir, { offeredAt: now().toISOString() });
|
|
313
|
+
else if (!offered && current.offeredAt)
|
|
314
|
+
patchSetupIntroState(dir, { offeredAt: undefined });
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
log("warn", `setup intro state not updated: ${err instanceof Error ? err.message : String(err)}`);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
299
320
|
// Scheduled runs are reported in the owner's own zone: "every weekday at 9am"
|
|
300
321
|
// was set up here, so "due 13:00 UTC" would read as the wrong time.
|
|
301
322
|
const timeZone = machineTimeZone();
|
|
@@ -1105,6 +1126,7 @@ export async function runBot(deps) {
|
|
|
1105
1126
|
return;
|
|
1106
1127
|
}
|
|
1107
1128
|
clearInflight(dir, KICKOFF_MESSAGE_ID);
|
|
1129
|
+
noteSetupReply(outcome.reply);
|
|
1108
1130
|
if (readBotConfig(dir)?.kickoffPendingSignIn)
|
|
1109
1131
|
patchBotConfig(dir, { kickoffPendingSignIn: false });
|
|
1110
1132
|
const st = readStatus();
|
|
@@ -1323,6 +1345,54 @@ export async function runBot(deps) {
|
|
|
1323
1345
|
await finish("failed", 0, `${FAILURE_LIMIT} model failures in 10 minutes; last: ${outcome.excerpt}`);
|
|
1324
1346
|
}
|
|
1325
1347
|
};
|
|
1348
|
+
/**
|
|
1349
|
+
* The instant answer to "Explain HQ first" (setup-explain.ts). Returns true
|
|
1350
|
+
* when it answered the message (sent, marked processed, acked); false sends
|
|
1351
|
+
* the message down the normal model path, so nothing here can leave a
|
|
1352
|
+
* message unanswered.
|
|
1353
|
+
*/
|
|
1354
|
+
const sendSetupExplanation = async (item, text, attachments) => {
|
|
1355
|
+
const id = item.messageId;
|
|
1356
|
+
let reply;
|
|
1357
|
+
try {
|
|
1358
|
+
const due = setupExplainDue({
|
|
1359
|
+
text,
|
|
1360
|
+
attachments,
|
|
1361
|
+
state: readSetupIntroState(dir),
|
|
1362
|
+
progress: readSetupProgressIntro(resolveBotMemoryDir(config)),
|
|
1363
|
+
});
|
|
1364
|
+
if (!due)
|
|
1365
|
+
return false;
|
|
1366
|
+
const explainer = loadSetupExplainer(config.hqRoot, config.workerDir);
|
|
1367
|
+
if (!explainer) {
|
|
1368
|
+
log("warn", `no usable explain-hq.md in ${config.workerDir}; answering ${id} with a model turn`);
|
|
1369
|
+
return false;
|
|
1370
|
+
}
|
|
1371
|
+
reply = explainer.reply;
|
|
1372
|
+
}
|
|
1373
|
+
catch (err) {
|
|
1374
|
+
log("warn", `setup explanation check failed (${err instanceof Error ? err.message : String(err)}); answering ${id} with a model turn`);
|
|
1375
|
+
return false;
|
|
1376
|
+
}
|
|
1377
|
+
try {
|
|
1378
|
+
await sendToTarget({ kind: "dm", peerUid: config.ownerUid }, reply, item.rootEventId, "human");
|
|
1379
|
+
}
|
|
1380
|
+
catch (err) {
|
|
1381
|
+
log("warn", `setup explanation for ${id} could not be sent (${err instanceof Error ? err.message : String(err)}); answering with a model turn`);
|
|
1382
|
+
return false;
|
|
1383
|
+
}
|
|
1384
|
+
markProcessed(dir, id, now().toISOString());
|
|
1385
|
+
try {
|
|
1386
|
+
patchSetupIntroState(dir, { explainedAt: now().toISOString(), notePending: true, offeredAt: undefined });
|
|
1387
|
+
}
|
|
1388
|
+
catch (err) {
|
|
1389
|
+
log("warn", `setup intro state not updated: ${err instanceof Error ? err.message : String(err)}`);
|
|
1390
|
+
}
|
|
1391
|
+
await api.ackInbox(config.agentUid, id);
|
|
1392
|
+
setStatus({ repliesSent: (readStatus()?.repliesSent ?? 0) + 1, lastActivityAt: now().toISOString() });
|
|
1393
|
+
log("info", `answered ${id} with the setup explanation (no model turn)`);
|
|
1394
|
+
return true;
|
|
1395
|
+
};
|
|
1326
1396
|
const handleItem = async (item) => {
|
|
1327
1397
|
const id = item.messageId;
|
|
1328
1398
|
const inboxState = readInboxState(dir);
|
|
@@ -1381,6 +1451,11 @@ export async function runBot(deps) {
|
|
|
1381
1451
|
await api.ackInbox(config.agentUid, id);
|
|
1382
1452
|
return;
|
|
1383
1453
|
}
|
|
1454
|
+
// Setup bot: "Explain HQ first" clicked while that question is pending is
|
|
1455
|
+
// answered with the fixed explanation, with no model turn.
|
|
1456
|
+
const ownerDm = target.kind === "dm" && target.peerUid === config.ownerUid;
|
|
1457
|
+
if (isSetupBot && ownerDm && (await sendSetupExplanation(item, messageText, attachmentRefs.length)))
|
|
1458
|
+
return;
|
|
1384
1459
|
const attempt = (attempts.get(id) ?? 0) + 1;
|
|
1385
1460
|
attempts.set(id, attempt);
|
|
1386
1461
|
const reactionId = item.eventId ?? id;
|
|
@@ -1441,7 +1516,10 @@ export async function runBot(deps) {
|
|
|
1441
1516
|
text = text ? `${text}\n\n${block}` : block;
|
|
1442
1517
|
}
|
|
1443
1518
|
const sessionScope = target.kind === "room" ? target.sessionScope : dmSessionScope(target.peerUid, config.ownerUid);
|
|
1444
|
-
|
|
1519
|
+
// The turn after the app sent the explanation: tell the model it went out.
|
|
1520
|
+
const setupNote = isSetupBot && ownerDm && readSetupIntroState(dir).notePending === true;
|
|
1521
|
+
const ownerText = setupNote ? `${SETUP_EXPLAINED_NOTE}\n\n${text}` : text;
|
|
1522
|
+
const prompt = target.kind === "room" ? await buildRoomTurn(item, target, text) : target.peerUid === config.ownerUid ? ownerText :
|
|
1445
1523
|
`[HQ DM sender: ${target.peerUid}; owner: ${config.ownerUid}; sender is not the owner.]\n\n${text}`;
|
|
1446
1524
|
const markerTarget = target.kind === "dm" ? { kind: "dm", peerUid: target.peerUid } : { kind: "room", channelId: target.channelId };
|
|
1447
1525
|
const { send, sendFinal, saved } = trackingSend(markerTarget, item.rootEventId);
|
|
@@ -1463,6 +1541,18 @@ export async function runBot(deps) {
|
|
|
1463
1541
|
? { afterProgressPost: () => api.postAgentStatus(target.channelId, ROOM_THINKING_STATUS, target.rootEventId) }
|
|
1464
1542
|
: {}),
|
|
1465
1543
|
});
|
|
1544
|
+
// The model saw the note once its turn ran. A turn that never started, or
|
|
1545
|
+
// one held for a coding-tool sign-in, runs again and keeps the note for it.
|
|
1546
|
+
if (setupNote && (outcome.ok || (outcome.started && !isRuntimeSignInFailure(outcome.excerpt)))) {
|
|
1547
|
+
try {
|
|
1548
|
+
patchSetupIntroState(dir, { notePending: false });
|
|
1549
|
+
}
|
|
1550
|
+
catch (err) {
|
|
1551
|
+
log("warn", `setup intro state not updated: ${err instanceof Error ? err.message : String(err)}`);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
if (outcome.ok && ownerDm)
|
|
1555
|
+
noteSetupReply(outcome.reply);
|
|
1466
1556
|
if (outcome.ok) {
|
|
1467
1557
|
// A no_reply sentinel or intentionally empty reply: ack silently, post nothing.
|
|
1468
1558
|
if (outcome.silent) {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The setup bot's instant "Explain HQ first" answer.
|
|
3
|
+
*
|
|
4
|
+
* The setup bot's first question is whether to explain HQ or jump straight in,
|
|
5
|
+
* with "Explain HQ first" as a suggested reply. The explanation is fixed text,
|
|
6
|
+
* so a click on that button does not need a model turn: the runner sends the
|
|
7
|
+
* text from the setup worker's `skills/explain-hq.md` at once, and the bot's
|
|
8
|
+
* next turn starts with a note saying the explanation already went out.
|
|
9
|
+
*
|
|
10
|
+
* The text has one home, the worker file. The model reads the same file when
|
|
11
|
+
* someone asks for the explanation in their own words, so the two paths never
|
|
12
|
+
* drift. Anything unexpected (file missing or malformed, the question not
|
|
13
|
+
* pending, any other message) falls back to the normal model turn.
|
|
14
|
+
*/
|
|
15
|
+
/** The suggested reply that triggers the instant answer. Exact match only. */
|
|
16
|
+
export declare const EXPLAIN_HQ_SUGGESTION = "Explain HQ first";
|
|
17
|
+
/** Where the explanation lives, relative to the setup worker folder. */
|
|
18
|
+
export declare const EXPLAIN_HQ_FILE: string;
|
|
19
|
+
/** The setup bot's progress note in its memory folder (setup worker.yaml). */
|
|
20
|
+
export declare const SETUP_PROGRESS_FILE = "setup-progress.md";
|
|
21
|
+
/** Runner-owned state, in the bot's own folder (never the model's memory). */
|
|
22
|
+
export declare const SETUP_INTRO_STATE_FILE = "setup-intro.json";
|
|
23
|
+
/**
|
|
24
|
+
* Put in front of the next message from the person, so the model knows the
|
|
25
|
+
* explanation went out without it. It never reaches the person.
|
|
26
|
+
*/
|
|
27
|
+
export declare const SETUP_EXPLAINED_NOTE: string;
|
|
28
|
+
export interface SetupExplainer {
|
|
29
|
+
/** The explanation, word for word (shared with the model path). */
|
|
30
|
+
explanation: string;
|
|
31
|
+
/** What the app adds after it: a line offering to continue, and suggestions. */
|
|
32
|
+
closing: string;
|
|
33
|
+
/** The whole message the app sends. */
|
|
34
|
+
reply: string;
|
|
35
|
+
}
|
|
36
|
+
/** Parse the worker file. Null when it is not the shape the app can send. */
|
|
37
|
+
export declare function parseSetupExplainer(body: string): SetupExplainer | null;
|
|
38
|
+
/** Read the explanation from the setup worker folder the bot runs. */
|
|
39
|
+
export declare function loadSetupExplainer(hqRoot: string, workerDir: string): SetupExplainer | null;
|
|
40
|
+
export type SetupProgressIntro = "asked" | "answered" | "finished" | null;
|
|
41
|
+
/** What the model's progress note says about the opening question. */
|
|
42
|
+
export declare function readSetupProgressIntro(memoryDir: string): SetupProgressIntro;
|
|
43
|
+
export interface SetupIntroState {
|
|
44
|
+
v: 1;
|
|
45
|
+
/** The bot's latest owner-DM reply asked the explain-or-jump question. */
|
|
46
|
+
offeredAt?: string;
|
|
47
|
+
/** The app sent the explanation (never sent twice). */
|
|
48
|
+
explainedAt?: string;
|
|
49
|
+
/** The model has not yet been told that the explanation went out. */
|
|
50
|
+
notePending?: boolean;
|
|
51
|
+
}
|
|
52
|
+
export declare function readSetupIntroState(dir: string): SetupIntroState;
|
|
53
|
+
export declare function patchSetupIntroState(dir: string, patch: Partial<Omit<SetupIntroState, "v">>): SetupIntroState;
|
|
54
|
+
/** A bot reply that asks the explain-or-jump question (the words or the button). */
|
|
55
|
+
export declare function replyOffersExplain(reply: string): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* The instant answer is due only when ALL of these hold: the message is exactly
|
|
58
|
+
* the button text with nothing attached, the question is pending (the bot's
|
|
59
|
+
* last reply offered it, or its note says it asked), and the explanation has
|
|
60
|
+
* not gone out yet (neither by the app nor, per the note, by the model).
|
|
61
|
+
*/
|
|
62
|
+
export declare function setupExplainDue(input: {
|
|
63
|
+
text: string;
|
|
64
|
+
attachments: number;
|
|
65
|
+
state: SetupIntroState;
|
|
66
|
+
progress: SetupProgressIntro;
|
|
67
|
+
}): boolean;
|
|
68
|
+
//# sourceMappingURL=setup-explain.d.ts.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The setup bot's instant "Explain HQ first" answer.
|
|
3
|
+
*
|
|
4
|
+
* The setup bot's first question is whether to explain HQ or jump straight in,
|
|
5
|
+
* with "Explain HQ first" as a suggested reply. The explanation is fixed text,
|
|
6
|
+
* so a click on that button does not need a model turn: the runner sends the
|
|
7
|
+
* text from the setup worker's `skills/explain-hq.md` at once, and the bot's
|
|
8
|
+
* next turn starts with a note saying the explanation already went out.
|
|
9
|
+
*
|
|
10
|
+
* The text has one home, the worker file. The model reads the same file when
|
|
11
|
+
* someone asks for the explanation in their own words, so the two paths never
|
|
12
|
+
* drift. Anything unexpected (file missing or malformed, the question not
|
|
13
|
+
* pending, any other message) falls back to the normal model turn.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
/** The suggested reply that triggers the instant answer. Exact match only. */
|
|
18
|
+
export const EXPLAIN_HQ_SUGGESTION = "Explain HQ first";
|
|
19
|
+
/** Where the explanation lives, relative to the setup worker folder. */
|
|
20
|
+
export const EXPLAIN_HQ_FILE = path.join("skills", "explain-hq.md");
|
|
21
|
+
/** The setup bot's progress note in its memory folder (setup worker.yaml). */
|
|
22
|
+
export const SETUP_PROGRESS_FILE = "setup-progress.md";
|
|
23
|
+
/** Runner-owned state, in the bot's own folder (never the model's memory). */
|
|
24
|
+
export const SETUP_INTRO_STATE_FILE = "setup-intro.json";
|
|
25
|
+
const EXPLANATION_HEADING = /^##[ \t]+The explanation[ \t]*$/m;
|
|
26
|
+
const CLOSING_HEADING = /^##[ \t]+After the explanation, when the app sends it[ \t]*$/m;
|
|
27
|
+
/**
|
|
28
|
+
* Put in front of the next message from the person, so the model knows the
|
|
29
|
+
* explanation went out without it. It never reaches the person.
|
|
30
|
+
*/
|
|
31
|
+
export const SETUP_EXPLAINED_NOTE = `[Note from the HQ app: the person clicked "${EXPLAIN_HQ_SUGGESTION}", and the app already sent them the explanation ` +
|
|
32
|
+
`from skills/explain-hq.md word for word, ending by asking whether to start the setup. The explain-or-jump question is answered. ` +
|
|
33
|
+
"Write `Intro: explained` in your progress note, do not explain HQ again, and carry on with the first unfinished setup step. " +
|
|
34
|
+
"Their reply to that message follows.]";
|
|
35
|
+
/** The text between a heading and the next `## ` heading (or the end). */
|
|
36
|
+
function sectionAfter(body, heading) {
|
|
37
|
+
const match = heading.exec(body);
|
|
38
|
+
if (!match)
|
|
39
|
+
return null;
|
|
40
|
+
const rest = body.slice(match.index + match[0].length);
|
|
41
|
+
const next = /^##[ \t]/m.exec(rest);
|
|
42
|
+
return (next ? rest.slice(0, next.index) : rest).trim();
|
|
43
|
+
}
|
|
44
|
+
/** True when the closing carries exactly one well-formed suggestions block. */
|
|
45
|
+
function hasSuggestionsBlock(closing) {
|
|
46
|
+
const fence = /^```hq-block\n(.+)\n```$/m.exec(closing);
|
|
47
|
+
if (!fence)
|
|
48
|
+
return false;
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(fence[1]);
|
|
51
|
+
const block = parsed.blocks?.[0];
|
|
52
|
+
return (parsed.v === 1 &&
|
|
53
|
+
block?.kind === "suggestions" &&
|
|
54
|
+
Array.isArray(block.items) &&
|
|
55
|
+
block.items.length >= 2 &&
|
|
56
|
+
block.items.every((item) => typeof item === "string" && item.trim().length > 0));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Parse the worker file. Null when it is not the shape the app can send. */
|
|
63
|
+
export function parseSetupExplainer(body) {
|
|
64
|
+
const text = body.replace(/\r\n/g, "\n");
|
|
65
|
+
const explanation = sectionAfter(text, EXPLANATION_HEADING);
|
|
66
|
+
const closing = sectionAfter(text, CLOSING_HEADING);
|
|
67
|
+
if (!explanation || !closing)
|
|
68
|
+
return null;
|
|
69
|
+
// The closing must end with the block (nothing after it) so the app shows the buttons.
|
|
70
|
+
if (!closing.endsWith("```") || !hasSuggestionsBlock(closing))
|
|
71
|
+
return null;
|
|
72
|
+
return { explanation, closing, reply: `${explanation}\n\n${closing}` };
|
|
73
|
+
}
|
|
74
|
+
/** Read the explanation from the setup worker folder the bot runs. */
|
|
75
|
+
export function loadSetupExplainer(hqRoot, workerDir) {
|
|
76
|
+
try {
|
|
77
|
+
return parseSetupExplainer(fs.readFileSync(path.resolve(hqRoot, workerDir, EXPLAIN_HQ_FILE), "utf8"));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** What the model's progress note says about the opening question. */
|
|
84
|
+
export function readSetupProgressIntro(memoryDir) {
|
|
85
|
+
let note;
|
|
86
|
+
try {
|
|
87
|
+
note = fs.readFileSync(path.join(memoryDir, SETUP_PROGRESS_FILE), "utf8");
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (/^Status:[ \t]*finished/im.test(note))
|
|
93
|
+
return "finished";
|
|
94
|
+
if (/^Intro:[ \t]*(explained|skipped)\b/im.test(note))
|
|
95
|
+
return "answered";
|
|
96
|
+
if (/^Intro:[ \t]*asked\b/im.test(note))
|
|
97
|
+
return "asked";
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
export function readSetupIntroState(dir) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(dir, SETUP_INTRO_STATE_FILE), "utf8"));
|
|
103
|
+
return parsed && parsed.v === 1 ? parsed : { v: 1 };
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return { v: 1 };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export function patchSetupIntroState(dir, patch) {
|
|
110
|
+
const next = { ...readSetupIntroState(dir), ...patch, v: 1 };
|
|
111
|
+
for (const key of Object.keys(next)) {
|
|
112
|
+
if (next[key] === undefined)
|
|
113
|
+
delete next[key];
|
|
114
|
+
}
|
|
115
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
116
|
+
const dest = path.join(dir, SETUP_INTRO_STATE_FILE);
|
|
117
|
+
const tmp = `${dest}.tmp.${process.pid}`;
|
|
118
|
+
fs.writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
119
|
+
fs.renameSync(tmp, dest);
|
|
120
|
+
return next;
|
|
121
|
+
}
|
|
122
|
+
/** A bot reply that asks the explain-or-jump question (the words or the button). */
|
|
123
|
+
export function replyOffersExplain(reply) {
|
|
124
|
+
return reply.includes(`"${EXPLAIN_HQ_SUGGESTION}"`) || /explain HQ to you/i.test(reply);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The instant answer is due only when ALL of these hold: the message is exactly
|
|
128
|
+
* the button text with nothing attached, the question is pending (the bot's
|
|
129
|
+
* last reply offered it, or its note says it asked), and the explanation has
|
|
130
|
+
* not gone out yet (neither by the app nor, per the note, by the model).
|
|
131
|
+
*/
|
|
132
|
+
export function setupExplainDue(input) {
|
|
133
|
+
if (input.text !== EXPLAIN_HQ_SUGGESTION || input.attachments > 0)
|
|
134
|
+
return false;
|
|
135
|
+
if (input.state.explainedAt)
|
|
136
|
+
return false;
|
|
137
|
+
if (input.progress === "answered" || input.progress === "finished")
|
|
138
|
+
return false;
|
|
139
|
+
return Boolean(input.state.offeredAt) || input.progress === "asked";
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=setup-explain.js.map
|
|
@@ -42,7 +42,7 @@ export declare const MERGE_COPIES_FLAG = "checkpoint.ontology.merge-copies";
|
|
|
42
42
|
* names differ only in spacing, case or punctuation (CO-65).
|
|
43
43
|
*/
|
|
44
44
|
export declare const MERGE_VARIANTS_FLAG = "checkpoint.ontology.merge-variants";
|
|
45
|
-
export type CopySkipReason = "no_original" | "original_invalid" | "identity_differs" | "not_in_duplicate_group" | "group_size" | "type_differs" | "names_differ" | "numbered_copy";
|
|
45
|
+
export type CopySkipReason = "no_original" | "original_invalid" | "identity_differs" | "not_in_duplicate_group" | "group_size" | "type_differs" | "names_differ" | "numbered_copy" | "not_listed";
|
|
46
46
|
export type PairKind = "numbered" | "variants";
|
|
47
47
|
export interface CopySkip {
|
|
48
48
|
copy: string;
|
|
@@ -55,6 +55,11 @@ export interface CopyPair {
|
|
|
55
55
|
/** Company-relative path of the original it merges into. */
|
|
56
56
|
original: string;
|
|
57
57
|
}
|
|
58
|
+
/** One entry of a `--only` list: an exact copy -> original pair. */
|
|
59
|
+
export interface ListedPair {
|
|
60
|
+
copy: string;
|
|
61
|
+
original: string;
|
|
62
|
+
}
|
|
58
63
|
export interface CopyPlan extends CopyPair {
|
|
59
64
|
add_aliases: string[];
|
|
60
65
|
/** Copy names the original lacks that another entity already holds, so they do not move. */
|
|
@@ -98,6 +103,8 @@ export interface MergeCopiesReport {
|
|
|
98
103
|
path: string;
|
|
99
104
|
error: string;
|
|
100
105
|
}[];
|
|
106
|
+
/** Listed entries the finder did not produce. Absent unless `--only` was set. */
|
|
107
|
+
only_unmatched?: ListedPair[];
|
|
101
108
|
/** Entity-file links whose target file does not exist. */
|
|
102
109
|
broken_links_before: number;
|
|
103
110
|
broken_links_after: number;
|
|
@@ -114,6 +121,8 @@ export interface MergeCopiesOptions {
|
|
|
114
121
|
pairKind?: PairKind;
|
|
115
122
|
/** Handle at most this many copies (sorted by path). */
|
|
116
123
|
limit?: number;
|
|
124
|
+
/** Handle only these exact pairs; every other found pair is skipped as `not_listed`. */
|
|
125
|
+
only?: ListedPair[];
|
|
117
126
|
now: Date;
|
|
118
127
|
deleteVaultKey?: DeleteVaultKey;
|
|
119
128
|
}
|
|
@@ -500,7 +500,10 @@ export async function mergeCopies(options) {
|
|
|
500
500
|
const index = buildEntityIndex(company.dir);
|
|
501
501
|
const pairKind = options.pairKind ?? "numbered";
|
|
502
502
|
const found = pairKind === "variants" ? findVariantPairs(index) : findCopyPairs(index);
|
|
503
|
-
const
|
|
503
|
+
const listed = options.only
|
|
504
|
+
? found.pairs.filter((pair) => options.only.some((entry) => entry.copy === pair.copy && entry.original === pair.original))
|
|
505
|
+
: found.pairs;
|
|
506
|
+
const pairs = options.limit !== undefined ? listed.slice(0, Math.max(0, options.limit)) : listed;
|
|
504
507
|
const before = scanLinks(company).broken;
|
|
505
508
|
const { plans, merges, finals, notReady } = plan(company, index, pairs, found.pairs, now);
|
|
506
509
|
const report = {
|
|
@@ -511,7 +514,19 @@ export async function mergeCopies(options) {
|
|
|
511
514
|
duplicate_groups_after: 0,
|
|
512
515
|
copies_found: found.pairs.length,
|
|
513
516
|
plans,
|
|
514
|
-
skipped:
|
|
517
|
+
skipped: options.only
|
|
518
|
+
? [
|
|
519
|
+
...found.skipped,
|
|
520
|
+
...found.pairs
|
|
521
|
+
.filter((pair) => !listed.includes(pair))
|
|
522
|
+
.map((pair) => ({ copy: pair.copy, reason: "not_listed" })),
|
|
523
|
+
]
|
|
524
|
+
: found.skipped,
|
|
525
|
+
...(options.only
|
|
526
|
+
? {
|
|
527
|
+
only_unmatched: options.only.filter((entry) => !found.pairs.some((pair) => pair.copy === entry.copy && pair.original === entry.original)),
|
|
528
|
+
}
|
|
529
|
+
: {}),
|
|
515
530
|
merged: [],
|
|
516
531
|
relinked: [],
|
|
517
532
|
deleted: [],
|
|
@@ -20,99 +20,12 @@ export declare const ONTOLOGY_FLAG = "checkpoint.ontology";
|
|
|
20
20
|
* 6.18.7 or later; see ./sync-client.ts.
|
|
21
21
|
*/
|
|
22
22
|
export declare const ONTOLOGY_SYNC_GATED_FLAG = "checkpoint.ontology.sync-gated";
|
|
23
|
-
export declare const COMPANY_BRIEF_FLAG = "checkpoint.ontology.company-brief";
|
|
24
|
-
/**
|
|
25
|
-
* Applies only when the payload's sibling backend is grok. On gold-v2, Claude
|
|
26
|
-
* with stripped inputs lost 0.139 recall; Grok with stripped inputs gained
|
|
27
|
-
* precision without losing recall. A machine that falls back from grok keeps
|
|
28
|
-
* the annotated inputs.
|
|
29
|
-
*/
|
|
30
|
-
export declare const IGNORE_ANNOTATIONS_FLAG = "checkpoint.ontology.ignore-annotations";
|
|
31
|
-
/**
|
|
32
|
-
* With `--backend auto`, `hq core checkpoint` tries grok first for this
|
|
33
|
-
* company. Read here, in the detached sibling, and recorded in the company's
|
|
34
|
-
* state directory, so the foreground checkpoint makes no network call.
|
|
35
|
-
*/
|
|
36
|
-
export declare const GROK_BACKEND_FLAG = "checkpoint.ontology.grok-backend";
|
|
37
23
|
/**
|
|
38
24
|
* Several writers in one company vault (design section 20): record written
|
|
39
25
|
* entity bytes in the outbox, merge back set-aside versions, and report
|
|
40
26
|
* duplicate entities.
|
|
41
27
|
*/
|
|
42
28
|
export declare const MULTI_WRITER_FLAG = "checkpoint.ontology.multi-writer";
|
|
43
|
-
/**
|
|
44
|
-
* A candidate that links to an existing entity adds no aliases to it; the plan
|
|
45
|
-
* carries `link_aliases: false` and apply honours it. CO-42: most aliases
|
|
46
|
-
* written through links on the CO-41 runs were other things the text named
|
|
47
|
-
* next to the entity (`Publisher` on indigo, `Stripe Connect` on Stripe), and
|
|
48
|
-
* entity-hints multiplied them (22 aliases on a flag-off run, 45 and 125 on two
|
|
49
|
-
* hinted runs).
|
|
50
|
-
*/
|
|
51
|
-
export declare const NO_LINK_ALIASES_FLAG = "checkpoint.ontology.no-link-aliases";
|
|
52
|
-
/**
|
|
53
|
-
* A person the input and the company's entities give only a first name for is
|
|
54
|
-
* named by that first name instead of being left out. CO-44: on the CO-41
|
|
55
|
-
* grok-4.6 runs the model read "not a first name alone" as "skip them", and
|
|
56
|
-
* dropped people such as Amir and Sebastian, whose own entities carry only a
|
|
57
|
-
* first name. People were 24 of the 138 missed gold entities over three runs.
|
|
58
|
-
* The plan carries `first_name_people: true`, and apply then defers a one-word
|
|
59
|
-
* person whose word starts an existing person's fuller name or alias, since
|
|
60
|
-
* the model cannot see that `John Smith` already exists.
|
|
61
|
-
*/
|
|
62
|
-
export declare const FIRST_NAME_PEOPLE_FLAG = "checkpoint.ontology.first-name-people";
|
|
63
|
-
/**
|
|
64
|
-
* A knowledge input does not link to a concept entity whose canonical name is
|
|
65
|
-
* also the canonical name of an entity of another type (`concept/hq-secrets`
|
|
66
|
-
* beside `project/hq-secrets`). The plan carries
|
|
67
|
-
* `skip_ambiguous_concept_links: true`, and apply rejects the link as
|
|
68
|
-
* `ambiguous_type`. CO-46: 34 of the 73 false positives on the knowledge
|
|
69
|
-
* fixtures were such concept links; skipping them raised precision from 0.733
|
|
70
|
-
* to 0.803 and cost recall 0.023 on a replay of six runs.
|
|
71
|
-
*/
|
|
72
|
-
export declare const SKIP_AMBIGUOUS_CONCEPT_LINKS_FLAG = "checkpoint.ontology.skip-ambiguous-concept-links";
|
|
73
|
-
/**
|
|
74
|
-
* A knowledge link to the weaker of two entities that share a canonical name
|
|
75
|
-
* across types is redirected to the sibling other entity files link to
|
|
76
|
-
* (`project/build-your-own-agi` beside `concept/build-your-own-agi`, or
|
|
77
|
-
* `project/hq-core 2` beside `project/hq-core` when a concept also holds the
|
|
78
|
-
* name). The plan carries `dominant_sibling_link: true`, and apply reports
|
|
79
|
-
* the move in `dominant_sibling_redirects` without writing the entity files.
|
|
80
|
-
* CO-60: a replay of six runs raised knowledge precision from 0.725 to 0.754
|
|
81
|
-
* and recall from 0.784 to 0.816, and never changed a create.
|
|
82
|
-
*/
|
|
83
|
-
export declare const DOMINANT_SIBLING_LINK_FLAG = "checkpoint.ontology.dominant-sibling-link";
|
|
84
|
-
/**
|
|
85
|
-
* A knowledge link to a non-person entity that 50 or more
|
|
86
|
-
* other entity files link to is rejected as `hub_under_mentioned` when the
|
|
87
|
-
* input names it fewer than 2 times (whole word, any case, canonical name
|
|
88
|
-
* only). The plan carries `hub_mention_floor: true`. CO-62: a replay of six
|
|
89
|
-
* runs raised knowledge precision from 0.753 to 0.781 at the same recall.
|
|
90
|
-
*/
|
|
91
|
-
export declare const HUB_MENTION_FLOOR_FLAG = "checkpoint.ontology.hub-mention-floor";
|
|
92
|
-
/**
|
|
93
|
-
* A knowledge concept candidate whose name is an alias of more than one
|
|
94
|
-
* entity, and which apply would defer as `ambiguous_alias`, links instead to
|
|
95
|
-
* the one project whose canonical name it is (`HQ` typed as a concept beside
|
|
96
|
-
* `project/HQ` and several concepts aliased `HQ`). The plan carries
|
|
97
|
-
* `concept_project_link: true`. CO-51: over six runs of the shipping config
|
|
98
|
-
* the model typed HQ as a concept in some runs and not others, and the
|
|
99
|
-
* deferral lost 61 gold links; a replay of this rule raised recall from 0.639
|
|
100
|
-
* to 0.709 at precision 0.747 to 0.731 (61 of the 63 added links are HQ).
|
|
101
|
-
*/
|
|
102
|
-
export declare const CONCEPT_PROJECT_LINK_FLAG = "checkpoint.ontology.concept-project-link";
|
|
103
|
-
/**
|
|
104
|
-
* A knowledge link to the company's own company entity, or a
|
|
105
|
-
* concept-project-link redirect, needs evidence that names the entity as a
|
|
106
|
-
* word: the candidate name or one of its aliases, not inside a path, domain,
|
|
107
|
-
* handle, tag or identifier (`indigoai-us/hq`, `getindigo.ai`,
|
|
108
|
-
* `scope: company:indigo`). The plan carries `named_evidence: true`, and apply
|
|
109
|
-
* rejects such a link as `unnamed_in_evidence`. CO-57: on the six CO-52 runs,
|
|
110
|
-
* 67 of 143 false positives were links to company/indigo and project/HQ, most
|
|
111
|
-
* on evidence that only held the name inside an identifier; a replay of this
|
|
112
|
-
* rule raised precision on the concept-project-link arm from 0.705 to 0.750
|
|
113
|
-
* and lost no gold link.
|
|
114
|
-
*/
|
|
115
|
-
export declare const NAMED_EVIDENCE_FLAG = "checkpoint.ontology.named-evidence";
|
|
116
29
|
/**
|
|
117
30
|
* CO-59: consume the files touched by company checkpoints that were queued
|
|
118
31
|
* behind a busy sibling (see queued-touches.ts). On, the plan adds the
|
|
@@ -250,7 +163,6 @@ export type PlanOutcome = {
|
|
|
250
163
|
companyBrief?: CompanyBriefResult;
|
|
251
164
|
};
|
|
252
165
|
export declare const READ_PATH_INSTRUCTION = "When an input has read_path, read that file (relative to the run directory) instead of path, with the same line range. It is the same text with an older annotator's entity lists and entity links removed.";
|
|
253
|
-
export declare const PERSON_NAME_INSTRUCTION = "Name a person by the full name the input or the company's entities give, not a first name alone.";
|
|
254
166
|
export declare const FIRST_NAME_PEOPLE_INSTRUCTION = "Name a person by the full name the input or the company's entities give. When neither gives more than a first name, name the person by that first name; do not leave them out.";
|
|
255
167
|
export declare function ledgerPath(hqRoot: string, company: OntologyCompany): string;
|
|
256
168
|
export declare function readLedger(hqRoot: string, company: OntologyCompany): Ledger | null;
|