zz_

app/cli/commands/repo-submit.ts

zzmq.dev@gmail.com/zz / app / cli / commands / repo-submit.ts @ 867419d4 Download
Ladder Summary
  • file The module implements the **`zz repo submit`** CLI command: it determines the files changed in the current working directory (either staged or the diff of the last commit), reads their contents, resolves the user (or a managed agent) to obtain an API token, and then posts a `changeset_review` ask (and a dependent `submit_changeset` ask) to the remote repository via the engine’s remote‑tool API, cleaning up the temporary identity token on exit. **Exported symbols** - `interface RepoSubmitOptions` – optional `message` and `as` fields for the command. - `async function repoSubmit(slug: string, laneId: string, askId: string, opts: RepoSubmitOptions): Promise<void>` – performs the whole submit workflow.
 // `zz repo submit <owner/repo> <lane-id> <ask-id>`: the human-contributor
 // counterpart to agent/scriptedWork.ts's runContributorWorkAsk -- posts
 // a changeset_review + a dependent submit_changeset ask, then completes
 // the work ask, exactly the same protocol sequence an LLM-driven agent
 // follows via commit_changeset/ask_post/ask_answer, just reading real
 // file content off disk (what you `git add`ed) instead of generating
 // it. No local clone convention assumed: run this from inside whatever
 // directory `zz repo clone` (or a plain `git clone` with your own
 // token) put the repo in -- it operates on the current working
 // directory's git index, same as any other git-adjacent CLI command.
 //
 // This does NOT commit anything itself -- posting a changeset_review
 // only proposes one. A Maintainer still has to `zz review` and approve
 // it before a coordinator's `zz work` run actually merges it (calls
 // commit_changeset for real). That two-step is the point: a raw push
 // bypasses per-changeset review entirely, which is exactly what this
 // path exists to go through instead.
 import { execFileSync } from "node:child_process";
 import { readFileSync } from "node:fs";
 import { resolveIdentity, releaseIdentity } from "../lib/identity.js";
 import { setActiveToken } from "../lib/api.js";
 import { callRemoteTool } from "../lib/remoteTools.js";
 import type { AskEntry, Lane } from "../../engine/types.js";
 
 function git(args: string[]): string {
  return execFileSync("git", args, { encoding: "utf8" }).trim();
 }
 
 export interface RepoSubmitOptions {
  message?: string;
  /** --as <name>: submit as a managed agent instead of yourself -- see WorkOptions.as (work.ts) / lib/identity.ts for the full resolution order. */
  as?: string;
 }
 
 export async function repoSubmit(slug: string, laneId: string, askId: string, opts: RepoSubmitOptions): Promise<void> {
  const identity = await resolveIdentity(opts.as);
  setActiveToken(identity.token);
 
  // --as mints a fresh token for this one-shot command -- clean it up
  // however it ends, including Ctrl+C mid-flight (no TUI/raw-mode
  // here, so a real SIGINT reaches this handler).
  let cleanedUp = false;
  const cleanup = async (): Promise<void> => {
  if (cleanedUp) return;
  cleanedUp = true;
  await releaseIdentity(identity);
  };
  process.on("SIGINT", () => void cleanup().then(() => process.exit(0)));
  process.on("SIGTERM", () => void cleanup().then(() => process.exit(0)));
 
  try {
  const staged = git(["diff", "--cached", "--name-only"])
  .split("\n")
  .filter(Boolean);
  if (staged.length === 0) {
  console.error("Nothing staged. Edit your files, `git add` the ones you changed, then run this again.");
  process.exitCode = 1;
  return;
  }
 
  const files = staged.map((path) => ({ path, content: readFileSync(path, "utf8") }));
  const baseCommit = git(["rev-parse", "HEAD"]);
  const message = opts.message ?? `Changes to ${staged.join(", ")}`;
 
  const lane = (await callRemoteTool(slug, "lane_get", { lane_id: laneId })) as Lane | null;
  if (!lane) throw new Error(`no such lane "${laneId}" in ${slug}`);
  const theAsk = lane.history.find((e): e is AskEntry => e.type === "ASK" && e.id === askId);
  if (!theAsk) throw new Error(`no such ask "${askId}" in ${slug}/${laneId}`);
  const taskDescription = (theAsk.payload as { description?: string } | undefined)?.description ?? JSON.stringify(theAsk.payload);
 
  const review = (await callRemoteTool(slug, "ask_post", {
  lane_id: laneId,
  kind: "changeset_review",
  from: identity.username,
  payload: { taskDescription, files, message, summary: message },
  depends_on: [],
  })) as AskEntry;
 
  await callRemoteTool(slug, "ask_post", {
  lane_id: laneId,
  kind: "submit_changeset",
  from: identity.username,
  payload: { files, message, summary: message, baseCommit },
  depends_on: [askId, review.id],
  });
 
  await callRemoteTool(slug, "ask_answer", {
  lane_id: laneId,
  ask_id: askId,
  actor: identity.username,
  action: "complete",
  payload: { summary: message },
  });
 
  console.log(`Submitted ${staged.length} file(s) for review (changeset_review ${review.id}) and marked ${askId} complete.`);
  console.log(`Next: a Maintainer needs to \`zz review\` and approve it; once granted, a coordinator's \`zz work\` run merges it for real.`);
  } finally {
  await cleanup();
  }
 }