@indigoai-us/hq-cli 5.288.0 → 5.290.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 +22 -0
- package/assets/bot-workers/setup/skills/explain-hq.md +35 -0
- package/assets/bot-workers/setup/worker.yaml +25 -23
- package/assets/scaffold/core/scripts/handoff-finalize.sh +670 -0
- package/assets/scaffold/core/scripts/handoff-open-steps.sh +81 -0
- package/assets/scaffold/core/scripts/handoff-post.sh +187 -0
- package/assets/scaffold/core/scripts/qmd-reindex-bg.sh +40 -0
- package/dist/command-catalog.generated.d.ts +51 -0
- package/dist/command-catalog.generated.js +63 -0
- package/dist/commands/core-handoff.d.ts +22 -0
- package/dist/commands/core-handoff.js +1240 -0
- package/dist/commands/core-ontology.d.ts +6 -1
- package/dist/commands/core-ontology.js +42 -7
- package/dist/commands/core.js +8 -1
- package/dist/commands/lanes.js +157 -5
- 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/entity-file.d.ts +21 -0
- package/dist/lib/checkpoint-ontology/entity-file.js +100 -0
- package/dist/lib/checkpoint-ontology/merge-copies.d.ts +34 -2
- package/dist/lib/checkpoint-ontology/merge-copies.js +541 -27
- package/dist/lib/lanes/store.d.ts +4 -2
- package/dist/lib/lanes/store.js +37 -15
- package/dist/lib/lanes/types.d.ts +6 -2
- package/dist/lib/lanes/types.js +1 -1
- package/dist/utils/self-update.d.ts +3 -0
- package/dist/utils/self-update.js +232 -1
- package/package.json +1 -1
|
@@ -33,6 +33,8 @@ export interface MergeCopiesCommandOptions {
|
|
|
33
33
|
variants?: boolean;
|
|
34
34
|
limit?: number;
|
|
35
35
|
only?: string;
|
|
36
|
+
/** Also re-point refs and links held today (flag merge-rewrite-refs). */
|
|
37
|
+
rewriteRefs?: boolean;
|
|
36
38
|
json?: boolean;
|
|
37
39
|
}
|
|
38
40
|
/** Reads a `--only` file: a JSON array of `{ copy, original }` company-relative paths. */
|
|
@@ -44,7 +46,10 @@ export declare function readListedPairs(file: string): ListedPair[];
|
|
|
44
46
|
* `checkpoint.ontology.merge-copies` and `checkpoint.ontology.multi-writer`.
|
|
45
47
|
* With `--variants`, merges two-file duplicate groups whose names differ only
|
|
46
48
|
* in case, spacing or punctuation instead, gated by
|
|
47
|
-
* `checkpoint.ontology.merge-variants`.
|
|
49
|
+
* `checkpoint.ontology.merge-variants`. `--rewrite-refs` (flag
|
|
50
|
+
* `checkpoint.ontology.merge-rewrite-refs`) also re-points signal refs,
|
|
51
|
+
* invoice and bill links, source and knowledge links, and carries external
|
|
52
|
+
* ids and web domains.
|
|
48
53
|
*/
|
|
49
54
|
export declare function runOntologyMergeCopies(hqRoot: string, slug: string | undefined, options: MergeCopiesCommandOptions, deps?: OntologyCommandDeps): Promise<string>;
|
|
50
55
|
export declare function registerCoreOntologyCommand(core: Command): void;
|
|
@@ -17,7 +17,7 @@ import { applyOntologyRun, ApplyError } from "../lib/checkpoint-ontology/apply.j
|
|
|
17
17
|
import { resolveOntologyCompany, WriteBoundsError } from "../lib/checkpoint-ontology/company.js";
|
|
18
18
|
import { findDuplicateGroups, summarizeMergeBack, } from "../lib/checkpoint-ontology/multi-writer.js";
|
|
19
19
|
import { buildEntityIndex } from "../lib/checkpoint-ontology/tree.js";
|
|
20
|
-
import { MERGE_COPIES_FLAG, MERGE_VARIANTS_FLAG, mergeCopies, } from "../lib/checkpoint-ontology/merge-copies.js";
|
|
20
|
+
import { MERGE_COPIES_FLAG, MERGE_REWRITE_REFS_FLAG, MERGE_VARIANTS_FLAG, mergeCopies, } from "../lib/checkpoint-ontology/merge-copies.js";
|
|
21
21
|
import { callDeleteEndpoint } from "./files.js";
|
|
22
22
|
import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
|
|
23
23
|
import { registerCoreOntologyNativeCommands } from "./core-ontology-native.js";
|
|
@@ -146,11 +146,12 @@ function mergeCopiesSummary(report) {
|
|
|
146
146
|
const held = report.plans.filter((plan) => plan.held).length;
|
|
147
147
|
const lines = [
|
|
148
148
|
`ontology merge-copies (${report.mode.replace("_", " ")}) in ${report.company} (${report.pair_kind}): ${report.plans.length} of ${report.copies_found} copies, ${held} held, ${report.skipped.length} skipped`,
|
|
149
|
-
` merged ${report.merged.length}, relinked ${report.relinked.length}, deleted ${report.deleted.length}, not ready ${report.not_ready.length}, errors ${report.errors.length}`,
|
|
149
|
+
` merged ${report.merged.length}, relinked ${report.relinked.length}${report.rewritten !== undefined ? `, rewritten ${report.rewritten.length}` : ""}, deleted ${report.deleted.length}, not ready ${report.not_ready.length}, errors ${report.errors.length}`,
|
|
150
150
|
` duplicate groups ${report.duplicate_groups_before} -> ${report.duplicate_groups_after}; broken entity links ${report.broken_links_before} -> ${report.broken_links_after}`,
|
|
151
151
|
];
|
|
152
|
-
for (const plan of report.plans.filter((entry) => entry.held))
|
|
153
|
-
lines.push(` held ${plan.copy}: ${plan.held}`);
|
|
152
|
+
for (const plan of report.plans.filter((entry) => entry.held)) {
|
|
153
|
+
lines.push(` held ${plan.copy}: ${plan.held}${plan.held === "external_id_conflict" ? ` (${plan.detail})` : ""}`);
|
|
154
|
+
}
|
|
154
155
|
for (const error of report.errors)
|
|
155
156
|
lines.push(` error ${error.path}: ${error.error}`);
|
|
156
157
|
for (const entry of report.only_unmatched ?? [])
|
|
@@ -176,7 +177,36 @@ export function readListedPairs(file) {
|
|
|
176
177
|
})) {
|
|
177
178
|
throw new OntologyUsageError("ontology: --only must be a non-empty JSON array of { copy, original } paths");
|
|
178
179
|
}
|
|
179
|
-
return parsed.map((entry) =>
|
|
180
|
+
return parsed.map((entry) => {
|
|
181
|
+
if (entry.resolve_external_ids === undefined)
|
|
182
|
+
return { copy: entry.copy, original: entry.original };
|
|
183
|
+
return { copy: entry.copy, original: entry.original, resolve_external_ids: readResolution(entry.copy, entry.resolve_external_ids) };
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/** `{ provider: { primary, namespace, object_type? } }`, every value a non-empty string. */
|
|
187
|
+
function readResolution(copy, raw) {
|
|
188
|
+
const usage = `ontology: --only entry ${copy}: resolve_external_ids must map each provider to { primary, namespace, object_type? } strings`;
|
|
189
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw) || Object.keys(raw).length === 0)
|
|
190
|
+
throw new OntologyUsageError(usage);
|
|
191
|
+
const out = {};
|
|
192
|
+
for (const [provider, value] of Object.entries(raw)) {
|
|
193
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
194
|
+
throw new OntologyUsageError(usage);
|
|
195
|
+
const record = value;
|
|
196
|
+
const nonEmpty = (field) => typeof field === "string" && field.length > 0;
|
|
197
|
+
if (!Object.keys(record).every((key) => key === "primary" || key === "namespace" || key === "object_type") ||
|
|
198
|
+
!nonEmpty(record.primary) ||
|
|
199
|
+
!nonEmpty(record.namespace) ||
|
|
200
|
+
(record.object_type !== undefined && !nonEmpty(record.object_type))) {
|
|
201
|
+
throw new OntologyUsageError(usage);
|
|
202
|
+
}
|
|
203
|
+
out[provider] = {
|
|
204
|
+
primary: record.primary,
|
|
205
|
+
namespace: record.namespace,
|
|
206
|
+
...(record.object_type !== undefined ? { object_type: record.object_type } : {}),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
180
210
|
}
|
|
181
211
|
/**
|
|
182
212
|
* `hq core ontology merge-copies`: merge `<slug> <n>.md` copies into their
|
|
@@ -185,7 +215,10 @@ export function readListedPairs(file) {
|
|
|
185
215
|
* `checkpoint.ontology.merge-copies` and `checkpoint.ontology.multi-writer`.
|
|
186
216
|
* With `--variants`, merges two-file duplicate groups whose names differ only
|
|
187
217
|
* in case, spacing or punctuation instead, gated by
|
|
188
|
-
* `checkpoint.ontology.merge-variants`.
|
|
218
|
+
* `checkpoint.ontology.merge-variants`. `--rewrite-refs` (flag
|
|
219
|
+
* `checkpoint.ontology.merge-rewrite-refs`) also re-points signal refs,
|
|
220
|
+
* invoice and bill links, source and knowledge links, and carries external
|
|
221
|
+
* ids and web domains.
|
|
189
222
|
*/
|
|
190
223
|
export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
|
|
191
224
|
if (options.apply && options.delete) {
|
|
@@ -200,7 +233,7 @@ export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
|
|
|
200
233
|
throw new OntologyUsageError(`ontology: company rejected: ${resolution.reason}`);
|
|
201
234
|
const company = resolution.company;
|
|
202
235
|
const readFlag = deps.readFlag ?? registryFlagReader;
|
|
203
|
-
for (const flag of [options.variants ? MERGE_VARIANTS_FLAG : MERGE_COPIES_FLAG, MULTI_WRITER_FLAG]) {
|
|
236
|
+
for (const flag of [options.variants ? MERGE_VARIANTS_FLAG : MERGE_COPIES_FLAG, MULTI_WRITER_FLAG, ...(options.rewriteRefs ? [MERGE_REWRITE_REFS_FLAG] : [])]) {
|
|
204
237
|
if (!(await readFlag(flag, company))) {
|
|
205
238
|
return options.json ? JSON.stringify({ company: company.slug, skipped: `${flag} off` }) : `ontology merge-copies: skipped (${flag} is off)`;
|
|
206
239
|
}
|
|
@@ -220,6 +253,7 @@ export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
|
|
|
220
253
|
...(options.variants ? { pairKind: "variants" } : {}),
|
|
221
254
|
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
|
222
255
|
...(only ? { only } : {}),
|
|
256
|
+
...(options.rewriteRefs ? { rewriteRefs: true } : {}),
|
|
223
257
|
...(deleteVaultKey ? { deleteVaultKey } : {}),
|
|
224
258
|
});
|
|
225
259
|
return options.json ? JSON.stringify(report, null, 2) : mergeCopiesSummary(report);
|
|
@@ -274,6 +308,7 @@ export function registerCoreOntologyCommand(core) {
|
|
|
274
308
|
.option("--variants", "merge two-file duplicate groups whose names differ only in case, spacing or punctuation (checkpoint.ontology.merge-variants)")
|
|
275
309
|
.option("--limit <n>", "handle at most this many copies", (value) => Number(value))
|
|
276
310
|
.option("--only <file>", "handle only the copy -> original pairs listed in this JSON file")
|
|
311
|
+
.option("--rewrite-refs", "also re-point signal refs, invoice, source and knowledge links to the survivor and carry external ids and web domains (checkpoint.ontology.merge-rewrite-refs)")
|
|
277
312
|
.option("--json", "print JSON")
|
|
278
313
|
.option("--hq-root <path>", "HQ installation to operate on")
|
|
279
314
|
.action(async (options) => {
|
package/dist/commands/core.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import { Option } from "commander";
|
|
32
32
|
import { registerCoreCheckpointCommand } from "./core-checkpoint.js";
|
|
33
33
|
import { registerCoreDelegateCommand } from "./core-delegate.js";
|
|
34
|
+
import { registerCoreHandoffCommand } from "./core-handoff.js";
|
|
34
35
|
import { registerCoreOntologyCommand } from "./core-ontology.js";
|
|
35
36
|
import { registerCoreKnowledgePrefsCommand } from "./core-knowledge-prefs.js";
|
|
36
37
|
import { registerCorePolicyCommand } from "./core-policy.js";
|
|
@@ -241,7 +242,7 @@ export const SCAFFOLD_ONLY_ASSETS = [
|
|
|
241
242
|
{ asset: "core/scripts/ontology-source-worklist.sh", root: "live", summary: "Rollback companion for native ontology source worklist" },
|
|
242
243
|
{ asset: "core/scripts/source-yaml-validate.sh", root: "cwd", summary: "Rollback companion for native source YAML validation" },
|
|
243
244
|
// Shared by native knowledge-prefs and the ontology candidate shell fallback.
|
|
244
|
-
{ asset: "core/scripts/knowledge-prefs.sh", root: "live", summary: "
|
|
245
|
+
{ asset: "core/scripts/knowledge-prefs.sh", root: "live", summary: "Sibling dependency for ontology candidate rollback companion" },
|
|
245
246
|
// Rollback companions for the default-off native knowledge and policy routes.
|
|
246
247
|
// policy-retire also runs the scaffold validator in-process when native, but
|
|
247
248
|
// its bundled shell fallback needs both helper assets at these exact paths.
|
|
@@ -250,6 +251,11 @@ export const SCAFFOLD_ONLY_ASSETS = [
|
|
|
250
251
|
{ asset: "core/scripts/read-policy-frontmatter.sh", root: "cwd", summary: "Rollback companion for native policy frontmatter" },
|
|
251
252
|
{ asset: "core/scripts/eval-trigger.sh", root: "live", summary: "Trigger grammar helper used by the policy validator companion" },
|
|
252
253
|
{ asset: ".claude/hooks/validate-policy-frontmatter.sh", root: "live", summary: "Policy validator companion used by policy-retire rollback" },
|
|
254
|
+
// Wave 2a handoff shells remain bundled as the default-off rollback path.
|
|
255
|
+
{ asset: "core/scripts/handoff-finalize.sh", root: "live", summary: "Rollback shell for native handoff finalize" },
|
|
256
|
+
{ asset: "core/scripts/handoff-post.sh", root: "live", summary: "Rollback shell for native handoff post" },
|
|
257
|
+
{ asset: "core/scripts/handoff-open-steps.sh", root: "live", summary: "Rollback shell for native handoff open-steps" },
|
|
258
|
+
{ asset: "core/scripts/qmd-reindex-bg.sh", root: "live", summary: "Rollback handoff reindex forwarder" },
|
|
253
259
|
...NATIVE_UTILITY_COMMANDS.map(({ asset, root, summary }) => ({
|
|
254
260
|
asset,
|
|
255
261
|
root,
|
|
@@ -326,6 +332,7 @@ export function registerCoreCommands(program) {
|
|
|
326
332
|
registerCoreOntologyCommand(core);
|
|
327
333
|
registerCoreCheckpointGateSettingCommand(core);
|
|
328
334
|
registerCoreDelegateCommand(core);
|
|
335
|
+
registerCoreHandoffCommand(core);
|
|
329
336
|
registerCoreKnowledgePrefsCommand(core);
|
|
330
337
|
registerCorePolicyCommand(core);
|
|
331
338
|
// Wave 2 handoff/worktree commands retain their byte-identical shell assets
|
package/dist/commands/lanes.js
CHANGED
|
@@ -79,6 +79,7 @@ export { MAX_LANE_DEPTH };
|
|
|
79
79
|
export const LANES_RUNS_REL = path.join("workspace", "lanes-runs");
|
|
80
80
|
export const LANE_REMOTE_MIRROR_SYNC_FLAG = "lanes.remote-done-mirror";
|
|
81
81
|
const REMOTE_MIRROR_REFRESH_BATCH_SIZE = 100;
|
|
82
|
+
const REMOTE_LANE_NOT_FOUND_GRACE_MS = 15 * 60_000;
|
|
82
83
|
/** The lane → mesh seam. Defined in lib/lanes/mesh-events.ts; the watcher shares it. */
|
|
83
84
|
export { emitLaneDecisionEvents };
|
|
84
85
|
export { LANE_OUTPOST_SNAPSHOT_CACHE_FLAG };
|
|
@@ -1129,6 +1130,7 @@ function normalizedLaneListQuery(query) {
|
|
|
1129
1130
|
...(hierarchy === undefined ? {} : { hierarchy }),
|
|
1130
1131
|
...(query.snapshot ? { snapshot: true } : {}),
|
|
1131
1132
|
...(query.strictSnapshot ? { strictSnapshot: true } : {}),
|
|
1133
|
+
...(query.includeForeignHostRows ? { includeForeignHostRows: true } : {}),
|
|
1132
1134
|
};
|
|
1133
1135
|
}
|
|
1134
1136
|
async function readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOptions = {}) {
|
|
@@ -1191,7 +1193,7 @@ async function readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOpt
|
|
|
1191
1193
|
// Outpost's own store, so preserve those local identities for accounting.
|
|
1192
1194
|
const hasLocalHostIdentity = rowHost.kind === "local" &&
|
|
1193
1195
|
typeof rowHost.id === "string" && rowHost.id.trim() !== "";
|
|
1194
|
-
if (!belongsToOutpost && !hasLocalHostIdentity)
|
|
1196
|
+
if (!belongsToOutpost && !hasLocalHostIdentity && !query.includeForeignHostRows)
|
|
1195
1197
|
continue;
|
|
1196
1198
|
try {
|
|
1197
1199
|
lanes.push(parseLaneRecord(normalizeLaneRecordView(raw)));
|
|
@@ -1216,6 +1218,7 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds) {
|
|
|
1216
1218
|
const result = {
|
|
1217
1219
|
updated: new Map(),
|
|
1218
1220
|
unverifiedRemote: new Map(),
|
|
1221
|
+
missingOnOwner: new Map(),
|
|
1219
1222
|
};
|
|
1220
1223
|
const host = reconcileHostFromCtx(ctx);
|
|
1221
1224
|
if (ctx.env.HQ_LANES_REMOTE_READ === "1" || parentLaneIds?.size === 0)
|
|
@@ -1285,6 +1288,7 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds) {
|
|
|
1285
1288
|
laneIds: batch.map((lane) => lane.lane_id),
|
|
1286
1289
|
snapshot: true,
|
|
1287
1290
|
strictSnapshot: true,
|
|
1291
|
+
includeForeignHostRows: true,
|
|
1288
1292
|
});
|
|
1289
1293
|
}
|
|
1290
1294
|
catch (error) {
|
|
@@ -1306,6 +1310,8 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds) {
|
|
|
1306
1310
|
!ownerIsHostedByOutpost ||
|
|
1307
1311
|
(owner.company?.uid ?? null) !== (mirror.company?.uid ?? null) ||
|
|
1308
1312
|
(owner.company?.slug ?? null) !== (mirror.company?.slug ?? null)) {
|
|
1313
|
+
if (matches.length === 0)
|
|
1314
|
+
result.missingOnOwner.set(mirror.lane_id, mirror);
|
|
1309
1315
|
const detail = matches.length === 0
|
|
1310
1316
|
? "owner did not return the requested lane"
|
|
1311
1317
|
: matches.length > 1
|
|
@@ -1329,6 +1335,7 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds) {
|
|
|
1329
1335
|
}
|
|
1330
1336
|
const needsUpdate = before.state !== owner.state ||
|
|
1331
1337
|
before.queued_reason !== (owner.state === "queued" ? owner.queued_reason : undefined) ||
|
|
1338
|
+
JSON.stringify(before.queued_details) !== JSON.stringify(owner.state === "queued" ? owner.queued_details : undefined) ||
|
|
1332
1339
|
before.ended_at !== endedAt ||
|
|
1333
1340
|
before.envelope_summary !== envelopeSummary ||
|
|
1334
1341
|
(isTerminalLaneState(owner.state) && (JSON.stringify(before.last_envelope) !== JSON.stringify(owner.last_envelope) ||
|
|
@@ -1348,6 +1355,7 @@ async function refreshRemoteLaneMirrors(ctx, hqRoot, records, parentLaneIds) {
|
|
|
1348
1355
|
...current,
|
|
1349
1356
|
state: owner.state,
|
|
1350
1357
|
queued_reason: owner.state === "queued" ? owner.queued_reason : undefined,
|
|
1358
|
+
queued_details: owner.state === "queued" ? owner.queued_details : undefined,
|
|
1351
1359
|
ended_at: endedAt,
|
|
1352
1360
|
envelope_summary: envelopeSummary,
|
|
1353
1361
|
...(isTerminalLaneState(owner.state) ? {
|
|
@@ -2445,6 +2453,47 @@ function markDispatchFailure(hqRoot, laneId, details) {
|
|
|
2445
2453
|
};
|
|
2446
2454
|
});
|
|
2447
2455
|
}
|
|
2456
|
+
function retainQueuedOutpostAfterUnknownDelivery(hqRoot, laneId, outpostId, commandId) {
|
|
2457
|
+
return casUpdateLane(hqRoot, laneId, (current) => {
|
|
2458
|
+
if (current.state !== "queued" ||
|
|
2459
|
+
current.host.kind !== "outpost" ||
|
|
2460
|
+
current.host.id !== outpostId)
|
|
2461
|
+
return current;
|
|
2462
|
+
return {
|
|
2463
|
+
...current,
|
|
2464
|
+
queued_details: {
|
|
2465
|
+
reason: "outpost_dispatch_unknown",
|
|
2466
|
+
outcome_unknown: true,
|
|
2467
|
+
command_id: commandId,
|
|
2468
|
+
},
|
|
2469
|
+
};
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
function markRemoteLaneLost(hqRoot, laneId, outpostId, at, writer, operatorReason, expectedVersion) {
|
|
2473
|
+
return retryUpdateLane(hqRoot, laneId, writer, (current) => {
|
|
2474
|
+
if (current.host.kind !== "outpost" ||
|
|
2475
|
+
current.host.id !== outpostId ||
|
|
2476
|
+
(expectedVersion !== undefined && current.version !== expectedVersion) ||
|
|
2477
|
+
isTerminalLaneState(current.state))
|
|
2478
|
+
return current;
|
|
2479
|
+
return {
|
|
2480
|
+
...current,
|
|
2481
|
+
state: "failed",
|
|
2482
|
+
queued_reason: undefined,
|
|
2483
|
+
queued_details: undefined,
|
|
2484
|
+
terminal_reason: "remote_lost",
|
|
2485
|
+
terminal_details: {
|
|
2486
|
+
outpost_id: outpostId,
|
|
2487
|
+
owner_error: "lane_not_found",
|
|
2488
|
+
...(operatorReason === undefined ? {} : { operator_reason: operatorReason }),
|
|
2489
|
+
},
|
|
2490
|
+
stop_history: [
|
|
2491
|
+
...(current.stop_history ?? []),
|
|
2492
|
+
{ at, reason: "remote_lost" },
|
|
2493
|
+
],
|
|
2494
|
+
};
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2448
2497
|
/**
|
|
2449
2498
|
* The CAS updater a pickup failure applies. A lane that is already terminal
|
|
2450
2499
|
* for any reason (a cancel that landed after the caller's last read, an
|
|
@@ -3528,6 +3577,22 @@ async function dispatchCreatedOutpostLane(ctx, input) {
|
|
|
3528
3577
|
exit_code: dispatched.exitCode,
|
|
3529
3578
|
});
|
|
3530
3579
|
}
|
|
3580
|
+
const commandId = dispatched.commandId?.trim();
|
|
3581
|
+
if (dispatched.done !== true || !commandId) {
|
|
3582
|
+
const details = {
|
|
3583
|
+
outpost_id: outpostId,
|
|
3584
|
+
lane_id: lane.lane_id,
|
|
3585
|
+
command_id: commandId || null,
|
|
3586
|
+
status: dispatched.status,
|
|
3587
|
+
done: dispatched.done ?? false,
|
|
3588
|
+
exit_code: dispatched.exitCode,
|
|
3589
|
+
outcome_unknown: true,
|
|
3590
|
+
};
|
|
3591
|
+
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not confirm pickup for lane ${lane.lane_id}; the queued lane will be reconciled against its owner`, {
|
|
3592
|
+
...details,
|
|
3593
|
+
state: retainQueuedOutpostAfterUnknownDelivery(hqRoot, lane.lane_id, outpostId, commandId || null).state,
|
|
3594
|
+
});
|
|
3595
|
+
}
|
|
3531
3596
|
const stdoutTrimmed = dispatched.stdout.trim();
|
|
3532
3597
|
if (remote === null && stdoutTrimmed !== "" && dispatched.status !== "Success") {
|
|
3533
3598
|
if (preserveQueuedOnFailure) {
|
|
@@ -3606,6 +3671,9 @@ async function dispatchCreatedOutpostLane(ctx, input) {
|
|
|
3606
3671
|
catch (err) {
|
|
3607
3672
|
if (preserveQueuedOnFailure)
|
|
3608
3673
|
throw err;
|
|
3674
|
+
if (isLanesError(err) && err.error === "outpost_delivery_unconfirmed" && err.details.outcome_unknown === true) {
|
|
3675
|
+
throw err;
|
|
3676
|
+
}
|
|
3609
3677
|
if (isLanesError(err) &&
|
|
3610
3678
|
err.error === DISPATCH_TERMINAL_REASON &&
|
|
3611
3679
|
readLane(hqRoot, lane.lane_id).state === "failed") {
|
|
@@ -4551,7 +4619,7 @@ async function runList(ctx, opts, fastRouteFlags) {
|
|
|
4551
4619
|
}
|
|
4552
4620
|
const remoteRead = ctx.env[REMOTE_LANE_READ_ENV] === "1";
|
|
4553
4621
|
const remoteRefresh = remoteRead
|
|
4554
|
-
? { updated: new Map(), unverifiedRemote: new Map() }
|
|
4622
|
+
? { updated: new Map(), unverifiedRemote: new Map(), missingOnOwner: new Map() }
|
|
4555
4623
|
: await refreshRemoteLaneMirrors(ctx, hqRoot, laneSource, new Set(selectedLanes
|
|
4556
4624
|
.filter((lane) => !isTerminalLaneState(lane.state))
|
|
4557
4625
|
.map((lane) => lane.lane_id)));
|
|
@@ -7035,11 +7103,21 @@ async function readOutpostTerminalLane(ctx, laneId, outpostId, stopCommandId, wa
|
|
|
7035
7103
|
commandId,
|
|
7036
7104
|
};
|
|
7037
7105
|
}
|
|
7106
|
+
function isDefiniteOutpostLaneNotFound(dispatched, laneId, ownerError) {
|
|
7107
|
+
return dispatched.done === true &&
|
|
7108
|
+
Boolean(dispatched.commandId?.trim()) &&
|
|
7109
|
+
dispatched.status === "Failed" &&
|
|
7110
|
+
dispatched.exitCode !== null &&
|
|
7111
|
+
dispatched.exitCode !== 0 &&
|
|
7112
|
+
ownerError?.error === "lane_not_found" &&
|
|
7113
|
+
ownerError.details.lane_id === laneId;
|
|
7114
|
+
}
|
|
7038
7115
|
async function dispatchOutpostLaneStop(ctx, lane, reason, quiet = false) {
|
|
7039
7116
|
if (lane.host.kind !== "outpost") {
|
|
7040
7117
|
throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch stop to`, { lane_id: lane.lane_id });
|
|
7041
7118
|
}
|
|
7042
7119
|
const outpostId = lane.host.id;
|
|
7120
|
+
const hqRoot = ctx.hqRoot();
|
|
7043
7121
|
const waitMs = outpostSingleLaneWaitMs(ctx.env);
|
|
7044
7122
|
let dispatched;
|
|
7045
7123
|
try {
|
|
@@ -7063,7 +7141,36 @@ async function dispatchOutpostLaneStop(ctx, lane, reason, quiet = false) {
|
|
|
7063
7141
|
required_option: "--reason",
|
|
7064
7142
|
});
|
|
7065
7143
|
}
|
|
7144
|
+
const commandId = dispatched.commandId?.trim();
|
|
7066
7145
|
const ownerError = ownerErrorFromOutput(dispatched.stdout);
|
|
7146
|
+
if (isDefiniteOutpostLaneNotFound(dispatched, lane.lane_id, ownerError)) {
|
|
7147
|
+
const at = ctx.now().toISOString();
|
|
7148
|
+
const updated = markRemoteLaneLost(ctx.hqRoot(), lane.lane_id, outpostId, at, "command", reason);
|
|
7149
|
+
if (updated.terminal_reason !== "remote_lost") {
|
|
7150
|
+
throw new LanesError("already_terminal", `Lane ${lane.lane_id} became terminal before its missing Outpost record could be marked remote_lost`, { lane_id: lane.lane_id, state: updated.state });
|
|
7151
|
+
}
|
|
7152
|
+
const cancelled = cancelPendingQuestionsForLane(hqRoot, lane.lane_id, at, "remote_lost");
|
|
7153
|
+
await emitLaneTerminalStateSoft(ctx, updated);
|
|
7154
|
+
if (!quiet)
|
|
7155
|
+
printJson({
|
|
7156
|
+
ok: true,
|
|
7157
|
+
action: "stop",
|
|
7158
|
+
reason: "remote_lost",
|
|
7159
|
+
lane: updated,
|
|
7160
|
+
terminal: true,
|
|
7161
|
+
resumable: false,
|
|
7162
|
+
cancelled_questions: cancelled.map((question) => question.question_id),
|
|
7163
|
+
outpost_id: outpostId,
|
|
7164
|
+
outpost_command_id: commandId,
|
|
7165
|
+
outpost_result: {
|
|
7166
|
+
state: "failed",
|
|
7167
|
+
reason: "remote_lost",
|
|
7168
|
+
status: dispatched.status,
|
|
7169
|
+
exit_code: dispatched.exitCode,
|
|
7170
|
+
},
|
|
7171
|
+
});
|
|
7172
|
+
return;
|
|
7173
|
+
}
|
|
7067
7174
|
if (ownerError) {
|
|
7068
7175
|
throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} rejected the stop command: ${ownerError.message}`, {
|
|
7069
7176
|
lane_id: lane.lane_id,
|
|
@@ -7075,7 +7182,6 @@ async function dispatchOutpostLaneStop(ctx, lane, reason, quiet = false) {
|
|
|
7075
7182
|
owner_error: ownerError,
|
|
7076
7183
|
});
|
|
7077
7184
|
}
|
|
7078
|
-
const commandId = dispatched.commandId?.trim();
|
|
7079
7185
|
if (dispatched.done !== true || !commandId) {
|
|
7080
7186
|
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return a completed stop command for lane ${lane.lane_id}`, {
|
|
7081
7187
|
lane_id: lane.lane_id,
|
|
@@ -7559,8 +7665,42 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
|
|
|
7559
7665
|
const fromEnv = ctx.env.HQ_ROOT?.trim();
|
|
7560
7666
|
const hqRoot = fromEnv && ctx.existsSync(fromEnv) ? fromEnv : ctx.hqRoot();
|
|
7561
7667
|
const mirrorRefresh = opts.dryRun
|
|
7562
|
-
? {
|
|
7668
|
+
? {
|
|
7669
|
+
updated: new Map(),
|
|
7670
|
+
unverifiedRemote: new Map(),
|
|
7671
|
+
missingOnOwner: new Map(),
|
|
7672
|
+
}
|
|
7563
7673
|
: await refreshRemoteLaneMirrors(ctx, hqRoot, loadAllLanes(hqRoot));
|
|
7674
|
+
const remoteLost = [];
|
|
7675
|
+
const remoteLostErrors = [];
|
|
7676
|
+
if (!opts.dryRun && mirrorRefresh.missingOnOwner.size > 0) {
|
|
7677
|
+
const at = ctx.now().toISOString();
|
|
7678
|
+
const nowMs = Date.parse(at);
|
|
7679
|
+
for (const [laneId, mirror] of mirrorRefresh.missingOnOwner) {
|
|
7680
|
+
const createdAtMs = Date.parse(mirror.created_at);
|
|
7681
|
+
if (!Number.isFinite(createdAtMs) ||
|
|
7682
|
+
nowMs - createdAtMs < REMOTE_LANE_NOT_FOUND_GRACE_MS)
|
|
7683
|
+
continue;
|
|
7684
|
+
try {
|
|
7685
|
+
const updated = markRemoteLaneLost(hqRoot, laneId, mirror.host.id, at, "reconcile", undefined, mirror.version);
|
|
7686
|
+
if (updated.terminal_reason !== "remote_lost") {
|
|
7687
|
+
if (updated.version !== mirror.version) {
|
|
7688
|
+
mirrorRefresh.unverifiedRemote.set(laneId, "mirror changed after the owner snapshot");
|
|
7689
|
+
}
|
|
7690
|
+
continue;
|
|
7691
|
+
}
|
|
7692
|
+
remoteLost.push({ id: laneId, reason: "remote_lost" });
|
|
7693
|
+
cancelPendingQuestionsForLane(hqRoot, laneId, at, "remote_lost");
|
|
7694
|
+
await emitLaneTerminalStateSoft(ctx, updated);
|
|
7695
|
+
}
|
|
7696
|
+
catch (err) {
|
|
7697
|
+
remoteLostErrors.push({
|
|
7698
|
+
laneId,
|
|
7699
|
+
message: err instanceof Error ? err.message : String(err),
|
|
7700
|
+
});
|
|
7701
|
+
}
|
|
7702
|
+
}
|
|
7703
|
+
}
|
|
7564
7704
|
const summary = await runReconcilePass(hqRoot, {
|
|
7565
7705
|
now: ctx.now,
|
|
7566
7706
|
host: reconcileHostFromCtx(ctx),
|
|
@@ -7676,10 +7816,22 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
|
|
|
7676
7816
|
}
|
|
7677
7817
|
},
|
|
7678
7818
|
});
|
|
7819
|
+
for (const failure of remoteLostErrors) {
|
|
7820
|
+
summary.errors.push({
|
|
7821
|
+
code: "remote_lane_terminalize_failed",
|
|
7822
|
+
message: `Could not mark remote lane ${failure.laneId} lost: ${failure.message}`,
|
|
7823
|
+
});
|
|
7824
|
+
summary.ok = false;
|
|
7825
|
+
}
|
|
7826
|
+
const terminalizedRemoteIds = new Set(remoteLost.map(({ id }) => id));
|
|
7679
7827
|
printJson({
|
|
7680
7828
|
...summary,
|
|
7829
|
+
...(remoteLost.length > 0 ? { remote_lost: remoteLost } : {}),
|
|
7681
7830
|
...(mirrorRefresh.unverifiedRemote.size > 0
|
|
7682
|
-
? {
|
|
7831
|
+
? {
|
|
7832
|
+
unverified_remote_lanes: [...mirrorRefresh.unverifiedRemote.keys()]
|
|
7833
|
+
.filter((id) => !terminalizedRemoteIds.has(id)),
|
|
7834
|
+
}
|
|
7683
7835
|
: {}),
|
|
7684
7836
|
});
|
|
7685
7837
|
if (!summary.ok) {
|
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
|