@botbuddy/cli 1.64.13 → 1.64.15
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/bin/botbuddy.mjs +14 -2
- package/daemon/bin/darwin-arm64/botbuddy-daemon +0 -0
- package/daemon/bin/linux-x64/botbuddy-daemon +0 -0
- package/daemon-release/darwin-arm64.json +4 -4
- package/daemon-release/linux-x64.json +4 -4
- package/daemon-release/manifest.json +5 -5
- package/package.json +1 -1
- package/src/commands.mjs +10 -2
- package/src/docker-hygiene.mjs +2 -1
- package/src/feedback-outbox.mjs +119 -0
- package/src/feedback-reminder.mjs +16 -0
- package/src/feedback-safety.mjs +31 -0
- package/src/feedback.integration.fixture.mjs +103 -0
- package/src/feedback.mjs +269 -0
- package/src/run.mjs +175 -6
- package/src/stack-run-outcome.mjs +3 -1
- package/src/stack.mjs +32 -5
- package/src/test/playwright-reporter.mjs +3 -1
- package/src/test-lane-events.mjs +8 -1
- package/src/wait-core.mjs +17 -0
- package/src/wait-feedback-capture.mjs +62 -0
- package/src/wait.mjs +45 -0
package/src/stack.mjs
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
// `stop` itself and self-activates/finalizes via the fallback MCP tools.
|
|
29
29
|
|
|
30
30
|
import { writeLaneStatus } from "./lane-status.mjs";
|
|
31
|
+
import { markFeedbackGuidanceShown } from "./feedback-reminder.mjs";
|
|
31
32
|
import { spawn, spawnSync } from "child_process";
|
|
32
33
|
import { existsSync, lstatSync, readFileSync, realpathSync } from "fs";
|
|
33
34
|
import { open, unlink, readFile } from "fs/promises";
|
|
@@ -58,7 +59,7 @@ import { FORWARD_ATTESTATION_ENV, startForwardAttestation, readForwardAttestatio
|
|
|
58
59
|
import { stackServicesForForwards } from "./stack-service-discovery.mjs";
|
|
59
60
|
import { ENGINE_PUBLISHES_BEYOND_LOOPBACK, engineSocketIsLocal, formatPublishIp, isLoopbackIp, publishIpFix, resolvePublishIp } from "./docker-publish-ip.mjs";
|
|
60
61
|
import { bold, dim, yellow } from "./utils.mjs";
|
|
61
|
-
import { STACK_RELEASE_INTENT_ENV, observeStackRunSignal, ownerReleaseHeld, reportStackRunOutcome } from "./stack-run-outcome.mjs";
|
|
62
|
+
import { STACK_RELEASE_INTENT_ENV, classifyStackRunOutcome, observeStackRunSignal, ownerReleaseHeld, reportStackRunOutcome } from "./stack-run-outcome.mjs";
|
|
62
63
|
|
|
63
64
|
export const STACK_SCHEMA_VERSION = 1;
|
|
64
65
|
|
|
@@ -149,6 +150,10 @@ const STACK_READINESS_HELP = `${bold("READINESS")}
|
|
|
149
150
|
|
|
150
151
|
export const STACK_HELP = `${bold("botbuddy stack")} — one command for a batch-scoped local stack lease (BOT-1218)
|
|
151
152
|
|
|
153
|
+
If BotBuddy blocks your work, run bb feedback init --file .botbuddy/feedback-report.json,
|
|
154
|
+
fill in the failure and evidence, then run bb feedback submit --file .botbuddy/feedback-report.json.
|
|
155
|
+
Preserve the original lease and do not cancel or repeat it merely to report the problem.
|
|
156
|
+
|
|
152
157
|
${bold("USAGE")}
|
|
153
158
|
botbuddy stack up [options] Request a lease; park if the host is full; hold it active
|
|
154
159
|
botbuddy stack status --admission-request-id <uuid>
|
|
@@ -887,7 +892,7 @@ export function truncateReceipt(receipt, maxBytes = DEFAULT_RECEIPT_MAX_BYTES) {
|
|
|
887
892
|
const r = { ...receipt };
|
|
888
893
|
if (r.connection !== undefined) r.connection = { connection_truncated: true };
|
|
889
894
|
if (enc(r) <= maxBytes) return r;
|
|
890
|
-
|
|
895
|
+
const minimal = {
|
|
891
896
|
schema_version: r.schema_version, command: r.command, outcome: r.outcome,
|
|
892
897
|
lease_id: r.lease_id ?? null, state: r.state ?? null, exit_code: r.exit_code,
|
|
893
898
|
error: r.error ?? null, truncated: true,
|
|
@@ -896,6 +901,8 @@ export function truncateReceipt(receipt, maxBytes = DEFAULT_RECEIPT_MAX_BYTES) {
|
|
|
896
901
|
...(r.cleanup_code ? { cleanup_code: r.cleanup_code } : {}),
|
|
897
902
|
...(r.cleanup_error ? { cleanup_error: String(r.cleanup_error).slice(0, 160) } : {}),
|
|
898
903
|
};
|
|
904
|
+
const withFeedback = r.feedback_recovery ? { ...minimal, feedback_recovery: r.feedback_recovery } : minimal;
|
|
905
|
+
return enc(withFeedback) <= maxBytes ? withFeedback : minimal;
|
|
899
906
|
}
|
|
900
907
|
|
|
901
908
|
/** Resolve the requested stack directory once, before it leaves the coding
|
|
@@ -1442,6 +1449,12 @@ const AGENT_DEEPLINK_REQUIRED_GUIDANCE = "the control plane refused this stack b
|
|
|
1442
1449
|
|
|
1443
1450
|
/** The refusal text for a receipt: the server's message, else the CLI's guidance, else the code. */
|
|
1444
1451
|
export function leaseRefusalError(data) {
|
|
1452
|
+
if (data?.code === "STALE_AGENT_CONTEXT") {
|
|
1453
|
+
// These identifiers are shown only in the local receipt; never copy an
|
|
1454
|
+
// arbitrary server string into it or into the separate telemetry envelope.
|
|
1455
|
+
const ticket = (value) => typeof value === "string" && /^[A-Z][A-Z0-9]{1,15}-[0-9]{1,12}$/.test(value) ? value : "unknown";
|
|
1456
|
+
return `STALE_AGENT_CONTEXT: registered ticket ${ticket(data.registered_ticket_id)}; requested ticket ${ticket(data.requested_ticket_id)}. No lease was created. Run \`bb help register_agent\` to inspect the supported session registration options before retrying.`;
|
|
1457
|
+
}
|
|
1445
1458
|
if (data?.lease_id && ["STACK_CONTEXT_BIND_FAILED", "STACK_CONTEXT_ALREADY_BOUND"].includes(data?.code)) {
|
|
1446
1459
|
return `${data.message || data.code}; lease ${data.lease_id} is ${data.state ?? "unknown"}. ` +
|
|
1447
1460
|
`Inspect with \`bb stack status ${data.lease_id}\`; after the owning batch ends, release with \`bb stack done ${data.lease_id} --reap\`, ` +
|
|
@@ -5326,7 +5339,7 @@ export function runReceiptFields(result) {
|
|
|
5326
5339
|
* or the returned exit code.
|
|
5327
5340
|
*/
|
|
5328
5341
|
export async function cmdRun(opts, childArgv, {
|
|
5329
|
-
lifecycle = runStackLifecycle, report = reportStackRunOutcome, now = Date.now, env = process.env,
|
|
5342
|
+
lifecycle = runStackLifecycle, report = reportStackRunOutcome, now = Date.now, env = process.env, feedbackLog = (message) => process.stderr.write(message), markFeedback = markFeedbackGuidanceShown,
|
|
5330
5343
|
} = {}) {
|
|
5331
5344
|
// A caller that needs a wait selects this immutable UUID before dispatch.
|
|
5332
5345
|
// Ordinary foreground runs still receive a unique telemetry id, but it is not
|
|
@@ -5335,9 +5348,23 @@ export async function cmdRun(opts, childArgv, {
|
|
|
5335
5348
|
const startedAt = now();
|
|
5336
5349
|
const result = await lifecycle(opts, childArgv);
|
|
5337
5350
|
const fields = runReceiptFields(result);
|
|
5338
|
-
const
|
|
5351
|
+
const receipt = buildReceipt(fields);
|
|
5352
|
+
const failureClass = classifyStackRunOutcome(stackRunOutcomeFields(result), result.exitCode);
|
|
5353
|
+
const feedbackEligible = failureClass.category === "platform" || (failureClass.category === "invocation" && failureClass.code !== "INVALID_ARGUMENTS");
|
|
5354
|
+
if (feedbackEligible) {
|
|
5355
|
+
receipt.feedback_recovery = { status: "not_yet_checked", available_after: "command_exits", recover_command: "bb feedback recover", list_command: "bb call list_feedback" };
|
|
5356
|
+
}
|
|
5357
|
+
const code = emit(receipt, opts, result.exitCode);
|
|
5339
5358
|
try {
|
|
5340
|
-
await report({ fields: stackRunOutcomeFields(result), exitCode: code, runId, opts, env, durationMs: now() - startedAt, channel: result?.[OUTCOME_CHANNEL] ?? null });
|
|
5359
|
+
const outcome = await report({ fields: stackRunOutcomeFields(result), exitCode: code, runId, opts, env, durationMs: now() - startedAt, channel: result?.[OUTCOME_CHANNEL] ?? null });
|
|
5360
|
+
const capture = outcome?.feedback_capture;
|
|
5361
|
+
if (!opts?.json && capture?.status === "captured" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(capture.feedback_id ?? "")) {
|
|
5362
|
+
feedbackLog(`BotBuddy retained a failure report. Check it: bb feedback status --feedback-id ${capture.feedback_id}\nCreate a private draft: bb feedback init --file bb-feedback.json\nEdit the draft, then add your explanation: bb feedback enrich --feedback-id ${capture.feedback_id} --file bb-feedback.json\n`);
|
|
5363
|
+
markFeedback();
|
|
5364
|
+
} else if (!opts?.json && (capture?.status === "pending" || (outcome?.sent === false && feedbackEligible))) {
|
|
5365
|
+
feedbackLog("Automatic feedback capture was not confirmed and may still complete. Recover using the same identity: bb feedback recover\nThen check existing reports: bb call list_feedback\nIf the source event was not recorded, create a report: bb feedback init --file bb-feedback.json\n");
|
|
5366
|
+
markFeedback();
|
|
5367
|
+
}
|
|
5341
5368
|
} catch { /* best-effort: the receipt and exit code are already final */ }
|
|
5342
5369
|
return code;
|
|
5343
5370
|
}
|
|
@@ -68,13 +68,15 @@ export default class BotBuddyPlaywrightReporter {
|
|
|
68
68
|
|
|
69
69
|
onBegin(_config, suite) {
|
|
70
70
|
if (!this._path) return;
|
|
71
|
-
const tests = typeof suite?.allTests === "function" ? suite.allTests() :
|
|
71
|
+
const tests = typeof suite?.allTests === "function" ? suite.allTests() : null;
|
|
72
|
+
if (!Array.isArray(tests)) return;
|
|
72
73
|
tests.forEach((t, i) => {
|
|
73
74
|
const key = caseKey(t);
|
|
74
75
|
this._order.set(key, i);
|
|
75
76
|
const title = (typeof t?.titlePath === "function" ? t.titlePath() : []).filter(Boolean).join(" › ") || key;
|
|
76
77
|
this._append({ type: "begin", key, title, sort_order: i });
|
|
77
78
|
});
|
|
79
|
+
this._append({ type: "catalog_complete", case_count: tests.length });
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
onTestEnd(test, result) {
|
package/src/test-lane-events.mjs
CHANGED
|
@@ -14,12 +14,19 @@ export const SYNC_BATCH = 500;
|
|
|
14
14
|
export function parseLaneEvents(text, { warn = (m) => process.stderr.write(m) } = {}) {
|
|
15
15
|
const cases = new Map();
|
|
16
16
|
let malformed = 0;
|
|
17
|
+
let catalogComplete = false;
|
|
17
18
|
for (const raw of String(text ?? "").split("\n")) {
|
|
18
19
|
const line = raw.trim();
|
|
19
20
|
if (!line) continue;
|
|
20
21
|
let ev;
|
|
21
22
|
try { ev = JSON.parse(line); } catch { malformed += 1; warn("[botbuddy] skipping corrupt lane event line\n"); continue; }
|
|
23
|
+
if (ev?.type === "catalog_complete") {
|
|
24
|
+
catalogComplete = Number.isSafeInteger(ev.case_count) && ev.case_count >= 0 && ev.case_count === cases.size;
|
|
25
|
+
if (!catalogComplete) malformed += 1;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
22
28
|
if (!ev || typeof ev.key !== "string" || (ev.type !== "begin" && ev.type !== "verdict")) { malformed += 1; continue; }
|
|
29
|
+
if (!cases.has(ev.key)) catalogComplete = false;
|
|
23
30
|
const c = cases.get(ev.key) ?? { key: ev.key, title: ev.key, sort_order: cases.size, verdict: "pending", fail_reason: null, duration_ms: null };
|
|
24
31
|
if (typeof ev.title === "string" && ev.title) c.title = ev.title;
|
|
25
32
|
if (Number.isInteger(ev.sort_order)) c.sort_order = ev.sort_order;
|
|
@@ -30,7 +37,7 @@ export function parseLaneEvents(text, { warn = (m) => process.stderr.write(m) }
|
|
|
30
37
|
}
|
|
31
38
|
cases.set(ev.key, c);
|
|
32
39
|
}
|
|
33
|
-
return { cases, malformed };
|
|
40
|
+
return { cases, malformed, catalogComplete };
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
// Case map → the sync_test_run_cases payload, ordered by sort_order.
|
package/src/wait-core.mjs
CHANGED
|
@@ -225,6 +225,17 @@ const VALIDATORS = {
|
|
|
225
225
|
if (!CANONICAL_UUID_RE.test(jobId)) throw new Error("staging-pool-refill id must be a canonical UUID");
|
|
226
226
|
return { jobId };
|
|
227
227
|
},
|
|
228
|
+
// BOT-2888: a reporter waits for a revision after the last consumed update.
|
|
229
|
+
// The server binds the report to the registering agent and tenant.
|
|
230
|
+
feedback: (p, raw) => {
|
|
231
|
+
const match = /^\s*id\s*=\s*([0-9A-Fa-f-]+)\s*,\s*after\s*=\s*(0|[1-9][0-9]*)\s*$/.exec(raw ?? "");
|
|
232
|
+
if (!match || Object.keys(p).length !== 2 || p.id !== match[1] || String(p.after) !== match[2]) {
|
|
233
|
+
throw new Error("feedback needs exactly id=<feedback UUID>,after=<last consumed revision>");
|
|
234
|
+
}
|
|
235
|
+
const feedbackId = match[1].toLowerCase(), after = Number(match[2]);
|
|
236
|
+
if (!CANONICAL_UUID_RE.test(feedbackId) || !Number.isSafeInteger(after)) throw new Error("feedback requires a canonical UUID and safe nonnegative revision");
|
|
237
|
+
return { feedbackId, after };
|
|
238
|
+
},
|
|
228
239
|
// BOT-2629: this is deliberately a dedicated condition rather than a raw
|
|
229
240
|
// event escape hatch. The UUID is chosen before dispatch and the exact subject
|
|
230
241
|
// prevents another run on the same idle/re-adopted lease waking this waiter.
|
|
@@ -1309,6 +1320,11 @@ function conditionMatchesSignal(condition, signal, waitSessionId) {
|
|
|
1309
1320
|
signal.subject_key === `staging-pool-refill:${params.jobId}` &&
|
|
1310
1321
|
signal.payload?.job_id === params.jobId &&
|
|
1311
1322
|
["completed", "failed"].includes(signal.payload?.status);
|
|
1323
|
+
case "feedback":
|
|
1324
|
+
return signal.signal_type === "feedback_update" &&
|
|
1325
|
+
signal.subject_key === `feedback:${params.feedbackId}` &&
|
|
1326
|
+
signal.payload?.feedback_id === params.feedbackId &&
|
|
1327
|
+
Number.isSafeInteger(signal.payload?.revision) && signal.payload.revision > params.after;
|
|
1312
1328
|
case "stack-run":
|
|
1313
1329
|
return signal.signal_type === "stack_run" && signal.subject_key === `stack-run:${params.id}` &&
|
|
1314
1330
|
signal.payload?.run_id === params.id && Number.isInteger(signal.payload?.exit_code);
|
|
@@ -1523,6 +1539,7 @@ function conditionMatchesSignal(condition, signal, waitSessionId) {
|
|
|
1523
1539
|
export function signalTenantBlocked(signal, sessionTenant) {
|
|
1524
1540
|
if (sessionTenant === undefined) return false;
|
|
1525
1541
|
if (signal.tenant_id == null) return false;
|
|
1542
|
+
if (signal.signal_type === "feedback_update") return signal.tenant_id !== sessionTenant;
|
|
1526
1543
|
// Mirror the relay's precedence (recipient > owner > tenant): a signal that is
|
|
1527
1544
|
// owner- or recipient-scoped keeps THAT isolation and is never tenant-fenced, even
|
|
1528
1545
|
// though owner-scoped pr_review/ci_run also carry a tenant_id — otherwise a caller who
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Retain only the fixed failure code, and only when this exact session token
|
|
2
|
+
// binds to the cached agent and tenant. Replay re-verifies both identities.
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { cacheBelongsToHarness, findAgentStateRoot, harnessSessionIdFromEnv, readAgentState } from "./agent-state.mjs";
|
|
8
|
+
import { restrictWindowsAcl, restrictWindowsAclDir } from "./agent-state-fs.mjs";
|
|
9
|
+
import { queueFeedback } from "./feedback-outbox.mjs";
|
|
10
|
+
|
|
11
|
+
/** @param {any} options */
|
|
12
|
+
export async function captureWaitRegistrationFailure({ token, claim = false, cwd = process.cwd(),
|
|
13
|
+
loadState = readAgentState, rootFor = findAgentStateRoot, queue = queueFeedback,
|
|
14
|
+
harnessId = harnessSessionIdFromEnv(), uuid = randomUUID } = {}) {
|
|
15
|
+
try {
|
|
16
|
+
const state = await loadState(cwd);
|
|
17
|
+
if (!state || !cacheBelongsToHarness(state, harnessId) ||
|
|
18
|
+
typeof token !== "string" || token !== state.agent_session_token ||
|
|
19
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(state.agent_id ?? "") ||
|
|
20
|
+
!/^[a-z0-9][a-z0-9-]{0,63}$/.test(state.tenant ?? "")) return false;
|
|
21
|
+
const code = claim ? "claim_registration_failed" : "register_failed";
|
|
22
|
+
const result = await queue({ root: await rootFor(cwd), agentId: state.agent_id, tenant: state.tenant,
|
|
23
|
+
report: { idempotency_key: uuid(), kind: "unknown", surface: "wait", impact: "unknown",
|
|
24
|
+
title: claim ? "Wait claim registration failed" : "Wait registration failed", error_code: code,
|
|
25
|
+
actual: claim ? "The claim wait could not confirm a registered session." : "The wait could not confirm a registered session.",
|
|
26
|
+
evidence: [{ type: "text", value: `Wait receipt error: ${code}` }] } });
|
|
27
|
+
return result?.queued === true;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function captureClaimRegistrationFailure(options = {}) {
|
|
34
|
+
return captureWaitRegistrationFailure({ ...options, claim: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// With no proven reporter binding, save only fixed, non-identifying context.
|
|
38
|
+
// It is never replayed automatically: the agent must review it and choose its
|
|
39
|
+
// authenticated reporter and tenant before submitting.
|
|
40
|
+
export async function saveUnboundWaitDraft({ claim = false, tempRoot = tmpdir() } = {}) {
|
|
41
|
+
let directory;
|
|
42
|
+
try {
|
|
43
|
+
directory = await mkdtemp(join(tempRoot, "bb-feedback-wait-"));
|
|
44
|
+
if (process.platform === "win32") await restrictWindowsAclDir(directory);
|
|
45
|
+
const file = join(directory, "report.json");
|
|
46
|
+
const code = claim ? "claim_registration_failed" : "register_failed";
|
|
47
|
+
const report = {
|
|
48
|
+
idempotency_key: randomUUID(), kind: "unknown", surface: "wait", impact: "unknown",
|
|
49
|
+
title: claim ? "Wait claim registration failed" : "Wait registration failed",
|
|
50
|
+
error_code: code, goal: "", steps: "", expected: "",
|
|
51
|
+
actual: claim ? "The claim wait could not confirm a registered session." : "The wait could not confirm a registered session.",
|
|
52
|
+
blocked_reason: "", blocking_step: "", can_continue: null, safe_next_action: "", ticket_ref: "",
|
|
53
|
+
evidence: [{ type: "text", value: `Wait receipt error: ${code}` }],
|
|
54
|
+
};
|
|
55
|
+
await writeFile(file, `${JSON.stringify(report, null, 2)}\n`, { flag: "wx", mode: 0o600 });
|
|
56
|
+
if (process.platform === "win32") await restrictWindowsAcl(file);
|
|
57
|
+
return file;
|
|
58
|
+
} catch {
|
|
59
|
+
if (directory) await rm(directory, { recursive: true, force: true }).catch(() => {});
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/wait.mjs
CHANGED
|
@@ -22,6 +22,8 @@ import { AGENT_KEY_RE, readAgentSessionTokenEnv, legacyAgentAliasHint } from "./
|
|
|
22
22
|
import { clearAgentState, hasAgentSessionRuntime, isRejectedCachedSession, resolveAgentSessionCredential, selfHealAgentSession } from "./agent-session.mjs";
|
|
23
23
|
import { repairFailureRecovery } from "./agent-doctor.mjs";
|
|
24
24
|
import { touchAgentStateExpiry } from "./agent-state.mjs";
|
|
25
|
+
import { captureWaitRegistrationFailure, saveUnboundWaitDraft } from "./wait-feedback-capture.mjs";
|
|
26
|
+
import { markFeedbackGuidanceShown } from "./feedback-reminder.mjs";
|
|
25
27
|
import { SETUP_BLOCK } from "./setup-block.mjs";
|
|
26
28
|
import { createWaitCheckpointStore, WaitCheckpointError } from "./wait-checkpoint.mjs";
|
|
27
29
|
import { realpath } from "node:fs/promises";
|
|
@@ -56,6 +58,10 @@ Park until a condition holds, then exit ONCE with one JSON receipt on stdout. Ze
|
|
|
56
58
|
and zero tokens while parked. Every wait — time included — is a bb wait: never sleep loops,
|
|
57
59
|
until/while polling, watch, or gh run watch. Full guide: docs/agent-wait.md.
|
|
58
60
|
|
|
61
|
+
If BotBuddy blocks your work, run bb feedback init --file .botbuddy/feedback-report.json,
|
|
62
|
+
fill in the failure and evidence, then run bb feedback submit --file .botbuddy/feedback-report.json.
|
|
63
|
+
Preserve the original wait and do not cancel or repeat it merely to report the problem.
|
|
64
|
+
|
|
59
65
|
USAGE
|
|
60
66
|
bb wait [--any] <condition>... [options] arm a wait (also: bb help wait)
|
|
61
67
|
bb wait <subcommand> … inspect or recover a saved wait (SUBCOMMANDS)
|
|
@@ -169,6 +175,9 @@ CONDITIONS (TYPE:key=val,key=val — repeat for several; --any wakes on the fir
|
|
|
169
175
|
completed or failed. This is the only condition of its
|
|
170
176
|
wait and is available to each tenant agent that requested
|
|
171
177
|
the deduped job; its terminal receipt is safe to display.
|
|
178
|
+
feedback:id=<feedback UUID>,after=<revision>
|
|
179
|
+
Wait for a later update to your own feedback; use the
|
|
180
|
+
revision you last consumed and read/acknowledge updates.
|
|
172
181
|
stack-run:id=<run UUID> one named bb stack run reaching its terminal outcome
|
|
173
182
|
(BOT-2629). Choose the UUID BEFORE dispatch and pass it as
|
|
174
183
|
bb stack run --run-id <UUID>; the receipt carries exit_code,
|
|
@@ -654,6 +663,7 @@ async function registerWait(opts, conditions, deadlineIso) {
|
|
|
654
663
|
"command_run_single_condition_required",
|
|
655
664
|
"staging_pool_refill_unavailable",
|
|
656
665
|
"staging_pool_refill_single_condition_required",
|
|
666
|
+
"feedback_wait_unavailable",
|
|
657
667
|
]);
|
|
658
668
|
if (INVALID_CONDITION_CODES.has(body.error)) {
|
|
659
669
|
const err = new Error(body.detail || body.error);
|
|
@@ -902,6 +912,12 @@ async function finalizeWait(opts, waitSessionId, receipt, timeoutMs = 5000) {
|
|
|
902
912
|
process.stderr.write(`bb-wait: finalize (non-fatal) returned ${res.status}\n`);
|
|
903
913
|
return { delivered: false, error: body?.error || (res.ok ? "finalize_not_applied" : `finalize_http_${res.status}`) };
|
|
904
914
|
}
|
|
915
|
+
const capture = body?.feedback_capture;
|
|
916
|
+
if (capture?.status === "captured" && typeof capture.feedback_id === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(capture.feedback_id)) {
|
|
917
|
+
process.stderr.write(`bb-wait: failure context retained as feedback ${capture.feedback_id}. Create the file, then set kind, surface to wait, title, impact, goal, reproduction, expected and actual outcomes, blocking reason, whether you can continue, safe next action and evidence before enriching:\n bb feedback init --file .botbuddy/feedback-report.json\n bb feedback enrich --feedback-id ${capture.feedback_id} --file .botbuddy/feedback-report.json\n`);
|
|
918
|
+
} else if (capture?.status === "pending") {
|
|
919
|
+
process.stderr.write("bb-wait: feedback capture is pending. After reconnecting with the same agent and tenant, run: bb feedback recover\n");
|
|
920
|
+
}
|
|
905
921
|
return { delivered: true };
|
|
906
922
|
} catch (err) {
|
|
907
923
|
process.stderr.write(`bb-wait: finalize (non-fatal) failed: ${err && err.message || err}\n`);
|
|
@@ -1619,10 +1635,32 @@ async function sameWorktree(first, second) {
|
|
|
1619
1635
|
catch { return false; }
|
|
1620
1636
|
}
|
|
1621
1637
|
|
|
1638
|
+
function writeWaitFeedbackGuidance() {
|
|
1639
|
+
markFeedbackGuidanceShown();
|
|
1640
|
+
process.stderr.write("bb-wait: If this error blocks your work, preserve the receipt and report your goal, expected and actual outcomes, why you are blocked and evidence:\n bb feedback init --file .botbuddy/feedback-report.json\n bb feedback submit --file .botbuddy/feedback-report.json\nEdit the report before submitting; these instructions do not record feedback. If a partial draft is saved or queued below, use it instead of creating a duplicate. If the file exists, choose a new filename in both commands.\n");
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
async function preserveWaitRegistrationFeedback(token, claim) {
|
|
1644
|
+
if (await captureWaitRegistrationFailure({ token, claim })) {
|
|
1645
|
+
process.stderr.write("bb-wait: A partial draft was queued for this agent and tenant. Do not submit a duplicate. Reconnect and run:\n bb feedback flush\nUse the feedback_id from its receipt. Create and edit the details file with your goal, expected and actual outcomes and why you are blocked, then enrich the same report:\n bb feedback init --file .botbuddy/feedback-details.json\n bb feedback enrich --feedback-id <feedback_id> --file .botbuddy/feedback-details.json\n");
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
const file = await saveUnboundWaitDraft({ claim });
|
|
1649
|
+
if (!file) {
|
|
1650
|
+
process.stderr.write("bb-wait: Could not save a partial draft. Preserve the receipt and use the feedback init and submit commands above.\n");
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
const quoted = process.platform === "win32"
|
|
1654
|
+
? `'${file.replace(/'/g, "''")}'`
|
|
1655
|
+
: `'${file.replace(/'/g, "'\\''")}'`;
|
|
1656
|
+
process.stderr.write(`bb-wait: A private partial draft was saved without a verified reporter or tenant. Review and complete ${quoted}; verify the reporter and tenant before submitting. The OS may clear temporary files. Run:\n bb call whoami\n bb feedback submit --file ${quoted}\n`);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1622
1659
|
async function emitSavedWaitError(error) {
|
|
1623
1660
|
const code = error?.code ?? "wait_checkpoint_invalid";
|
|
1624
1661
|
const exitCode = code === "wait_checkpoint_write_failed" ? EXIT.INTERNAL : EXIT.INVALID;
|
|
1625
1662
|
process.stderr.write(`bb-wait: ${error?.message ?? code}\n`);
|
|
1663
|
+
writeWaitFeedbackGuidance();
|
|
1626
1664
|
await writeWaitReceipt({
|
|
1627
1665
|
schema_version: 1,
|
|
1628
1666
|
outcome: "error",
|
|
@@ -1835,6 +1873,7 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
|
|
|
1835
1873
|
// replayed — carries the lease-lost stderr line when it is a lease_lost terminal.
|
|
1836
1874
|
const emitReceipt = (receipt, options = {}) => {
|
|
1837
1875
|
writeLeaseLostNotice(receipt);
|
|
1876
|
+
if (receipt?.outcome === "error") writeWaitFeedbackGuidance();
|
|
1838
1877
|
return emit(receipt, { ...options, maxBytes: opts.receiptMaxBytes });
|
|
1839
1878
|
};
|
|
1840
1879
|
if (opts.help) return writeThenExit(HELP);
|
|
@@ -2302,6 +2341,7 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
|
|
|
2302
2341
|
if (hasClaim && waitSessionId == null) {
|
|
2303
2342
|
process.stderr.write("bb-wait: claim=true registration returned no wait_session_id; refusing to arm an unmatchable claim wait\n");
|
|
2304
2343
|
await emitReceipt({ schema_version: 1, outcome: "error", error: "claim_registration_failed" });
|
|
2344
|
+
await preserveWaitRegistrationFeedback(opts.token, true);
|
|
2305
2345
|
process.exit(EXIT.INTERNAL);
|
|
2306
2346
|
}
|
|
2307
2347
|
} catch (err) {
|
|
@@ -2456,6 +2496,7 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
|
|
|
2456
2496
|
if (hasClaim) {
|
|
2457
2497
|
process.stderr.write(`bb-wait: claim=true wait-session registration failed (failing closed, not arming untracked): ${err && err.message || err}\n`);
|
|
2458
2498
|
await emitReceipt({ schema_version: 1, outcome: "error", error: "claim_registration_failed" });
|
|
2499
|
+
await preserveWaitRegistrationFeedback(opts.token, true);
|
|
2459
2500
|
process.exit(EXIT.INTERNAL);
|
|
2460
2501
|
}
|
|
2461
2502
|
// BOT-1338/1608: every non-timer wait is a relay-tracked, session-token wait.
|
|
@@ -2477,6 +2518,7 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
|
|
|
2477
2518
|
null,
|
|
2478
2519
|
{ sessionTenant: sessionTenant ?? null, agentId: null },
|
|
2479
2520
|
));
|
|
2521
|
+
await preserveWaitRegistrationFeedback(opts.token, false);
|
|
2480
2522
|
process.exit(EXIT.INTERNAL);
|
|
2481
2523
|
}
|
|
2482
2524
|
}
|
|
@@ -2584,6 +2626,9 @@ export async function runWait(argv, { recoveryLocalWaitId = null } = {}) {
|
|
|
2584
2626
|
// BOT-2360: likewise `abandoned` — the relay refused the stream because the row is ended.
|
|
2585
2627
|
if (waitSessionId && !SERVER_ENDED_OUTCOMES.has(receipt.outcome)) {
|
|
2586
2628
|
const finalized = await finalizeWait(opts, waitSessionId, terminalReceipt);
|
|
2629
|
+
if (!finalized.delivered && terminalReceipt.outcome === "error" && typeof checkpoint.local_wait_id === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(checkpoint.local_wait_id)) {
|
|
2630
|
+
process.stderr.write(`bb-wait: the Wait receipt is saved locally, but cloud finalization was not confirmed. With the same agent and tenant, recover the saved Wait and then recover feedback:\n bb wait resume ${checkpoint.local_wait_id}\n bb feedback recover\n`);
|
|
2631
|
+
}
|
|
2587
2632
|
try {
|
|
2588
2633
|
checkpoint = await store.update(checkpoint.local_wait_id, finalized.delivered
|
|
2589
2634
|
? { cloud_finalize_pending: false, cloud_finalized_at: new Date().toISOString(), cloud_finalize_error: null }
|