/** * `arm` — the two mechanical halves of the arming ceremony. * * `arm` files a challenge, sends it, and returns immediately naming the * follow-up command. `arm --reply ""` verifies that * message and writes the arm marker for exactly the projects the challenge * recorded. Nothing waits anywhere: a reply sent to the project topic is * consumed mechanically by the orchestrator pane itself (#1061), and this * command remains the console's route for a reply that landed in the operator * DM — the in-session acknowledgement wait this replaced could never be * satisfied, because the console session runs no tick extension. * * Both halves print copy-pasteable commands, because the reader is usually an * agent in a console pane rather than a human at a prompt. */ import type { CommandContext } from "./context.ts"; import { armFleet, armReply, armTicks, type ArmChallengeSent } from "../fleet.ts"; import { withProgress } from "../ui/progress.ts"; export async function armCommand(ctx: CommandContext): Promise { const reply = replyArgument(ctx); if (reply !== undefined) { // The verification half is one store read and (on a match) the marker // writes: no send, no network, nothing to narrate. const result = armReply(reply, ctx.projectFlag); if (result.outcome === "refused") throw new Error(result.message); process.stdout.write( `ARMED — reply verified for owner ${result.owner}; ticks are now live for ` + `${String(result.armed.length)} project(s).\n` + result.armed .map( (project) => ` ${project.project ?? "this fleet"}: marker ${project.path}` + `${project.alreadyArmed ? " (replaced previous marker)" : ""}`, ) .join("\n") + `\nchallenge ${result.challengeId} is settled — the same code cannot arm anything again.\n`, ); return; } const targets = ctx.targetProjects(); // One ceremony for the whole fleet (#991). Two projects used to mean two // sequential handshakes with two codes in one chat, though nothing about the // fleet's state differed between them. `arm --project X` is untouched: it is // still exactly one project, one challenge, one marker. if (ctx.projectFlag === undefined && targets.length > 1) { const sent = await withProgress( "arm: sending one arming challenge for the fleet…", "Arming challenge sent", () => armFleet( targets.map((project) => project.name), { progress: (line) => process.stdout.write(`${line}\n`) }, ), { plainMessage: true }, ); process.stdout.write(challengeReceipt(sent)); return; } for (const project of targets) { // Proof-neutral wording: `claim-only` performs no Telegram send, so the // progress line cannot promise a challenge that never goes out (#613). The // result line names the proof that actually settled it. const result = await withProgress( "arm: verifying the arming proof…", "Arming proof settled", () => armTicks(project.name, { progress: (line) => process.stdout.write(`${line}\n`) }), { plainMessage: true }, ); if (result.outcome === "armed") { process.stdout.write( `ARMED — claim-only plumbing verdict proved; ticks are now live.\n` + result.armed .map( (armed) => `marker ${armed.path}${armed.alreadyArmed ? " (replaced previous marker)" : ""}`, ) .join("\n") + "\n", ); continue; } process.stdout.write(challengeReceipt(result)); } } /** * The challenge half's receipt: what was sent, how long it lasts, what a reply * will arm, and the exact command that arms it. The follow-up line is written * verbatim from the result so the printed command and the recorded transaction * can never disagree. */ function challengeReceipt(sent: ArmChallengeSent): string { return ( `CHALLENGE SENT — a code went to owner ${sent.owner}, valid for ${sent.validFor} ` + `(challenge ${sent.challengeId}).\n` + `NOTHING IS ARMED YET. A reply in the challenge's chat settles it automatically; ` + `on a console host run:\n` + ` ${sent.followUp}\n` + `That reply will arm ${String(sent.targets.length)} project(s):\n` + sent.targets .map((target) => ` ${target.project ?? "this fleet"}: marker ${target.armedFile}`) .join("\n") + "\n" ); } /** * The `--reply` payload, refusing the shapes that would otherwise send a fresh * challenge instead of verifying one. A bare `--reply` (no value) or an empty * string is a mistake worth naming: silently re-arming from a verification * attempt would leave the operator with two live codes and no idea why. */ function replyArgument(ctx: CommandContext): string | undefined { const value = ctx.flag("reply"); const present = ctx.argv.includes("--reply") || ctx.argv.some((token) => token.startsWith("--reply=")); if (!present) return undefined; if (value === undefined || value.trim().length === 0) { throw new Error( `arm --reply needs the operator's message, verbatim — for example: ` + `omp-conductor arm --reply "FLEET-1A2B3C4D"`, ); } return value; }