@officexapp/vidfarm-devcli 0.21.55 → 0.21.56

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.
@@ -0,0 +1,941 @@
1
+ // `vidfarm clipper-run` — the RUN MODE of an agentic clipper loop, and the
2
+ // per-task state that survives a context reset.
3
+ //
4
+ // A clipper task is a long-horizon job: read the brief, shortlist templates,
5
+ // pick one, plan it, procure raws, build, render, QA, get the cut reviewed, then
6
+ // submit. That is far more than a weak model (Antigravity, Gemini Flash, GLM,
7
+ // DeepSeek) holds in one context, and the failure is always the same shape: it
8
+ // forgets which stage it was at, re-does a step, or — worst — submits a cut the
9
+ // gigworker never watched. So the stage lives in a file, not in the context.
10
+ //
11
+ // FOUR RUN MODES, one axis: how much of the gigworker's attention each task
12
+ // costs. They differ ONLY in which stages stop for a human.
13
+ //
14
+ // full-interactive shortlist · plan · raws · cut — max quality on a weak
15
+ // model. The human picks the template, reads the plan, can
16
+ // help collect raws, and watches the cut.
17
+ // quick-interactive shortlist · cut — the human approves ONE
18
+ // template_id off a shortlist, the agent then runs the task
19
+ // alone, and the human watches the cut before it is sent.
20
+ // auto-batch cut — the agent picks the
21
+ // template and builds every task unattended; nothing is
22
+ // submitted until the human reviews the batch.
23
+ // auto-submit (nothing) — fully unattended,
24
+ // submits its own proofs. Opt-in, in the gigworker's own
25
+ // words, and reverted after any rejection.
26
+ //
27
+ // `quick-interactive` and `auto-batch` are the two common ones.
28
+ //
29
+ // STATE ON DISK, under the mission folder (default ./CLIPPER):
30
+ // run.json the chosen run mode
31
+ // tasks/<task-id>/state.json one task's stage, gate, shortlist, artifacts
32
+ // tasks/<task-id>/raws/ where this task's footage is collected
33
+ // REVIEW_QUEUE.md regenerated from every open gate — the human's list
34
+ // LEDGER.jsonl appended on submit / drop
35
+ //
36
+ // Backend-free (Node built-ins only) so it ships in the public cloud-only CLI.
37
+ import { parseArgs } from "node:util";
38
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
39
+ import path from "node:path";
40
+ const BOLD = "\x1b[1m";
41
+ const DIM = "\x1b[2m";
42
+ const GREEN = "\x1b[32m";
43
+ const YELLOW = "\x1b[33m";
44
+ const RESET = "\x1b[0m";
45
+ export const RUN_MODES = ["full-interactive", "quick-interactive", "auto-batch", "auto-submit"];
46
+ /** Nobody has chosen yet. Safe, and one of the two common modes. */
47
+ export const DEFAULT_RUN_MODE = "quick-interactive";
48
+ /** The pipeline, in order. `done`/`discarded` are terminal. */
49
+ export const STAGES = ["claimed", "shortlist", "plan", "raws", "build", "cut", "submit", "done"];
50
+ /**
51
+ * The whole difference between the four modes: which stages STOP for a human.
52
+ * Everything else in this file is the same code on every mode.
53
+ */
54
+ export const MODE_GATES = {
55
+ "full-interactive": ["shortlist", "plan", "raws", "cut"],
56
+ "quick-interactive": ["shortlist", "cut"],
57
+ "auto-batch": ["cut"],
58
+ "auto-submit": []
59
+ };
60
+ export const MODE_BLURB = {
61
+ "full-interactive": "Full interactive — four stops. The agent shortlists template_ids and the gigworker picks one; the agent " +
62
+ "writes the plan and the gigworker reviews it; the agent opens a local raws folder the gigworker may drop " +
63
+ "footage into; the agent builds and renders and the gigworker watches the cut before it is submitted. " +
64
+ "This is how a WEAK model produces work a buyer keeps: the human supplies the taste at four cheap moments.",
65
+ "quick-interactive": "Quick interactive — one stop before the work, one after. The agent suggests a few template_ids, the " +
66
+ "gigworker approves ONE, and the agent then plans and builds the whole task alone from that starting " +
67
+ "point. The gigworker watches the finished cut before it is submitted. One of the two most-used modes.",
68
+ "auto-batch": "Full auto with batch review — the agent picks the template and completes every task unattended, and " +
69
+ "submits NOTHING. Finished cuts pile up in REVIEW_QUEUE.md; the gigworker watches the batch in one " +
70
+ "sitting and approves or drops each one. The other most-used mode.",
71
+ "auto-submit": "Full auto with auto-submit — no human anywhere. The agent picks, builds and submits its own proofs. " +
72
+ "Only ever set by the gigworker, in their own words, and reverted to auto-batch after any rejection: a " +
73
+ "low_quality rejection is scored on the wallet and buyers read it as a trust score on the public profile."
74
+ };
75
+ /** What the human is asked at each gate, when the caller gives no `--ask`. */
76
+ const DEFAULT_ASK = {
77
+ shortlist: "Which of these templates should I adapt? Reply with one template_id (or say 'you pick').",
78
+ plan: "Does this plan match what the buyer asked for? Approve it, or tell me what to change.",
79
+ raws: "Want to help collect footage? Drop clips into the folder above, then approve. Say 'go' to let me source it all myself.",
80
+ cut: "Watch the cut: does it satisfy the brief, is the hook alive in the first 3 seconds, are the captions readable and inside the safe zone, is the audio clean, is the buyer's brand right?"
81
+ };
82
+ export function normalizeRunMode(raw) {
83
+ const v = (raw ?? "").trim().toLowerCase().replace(/[\s_]+/g, "-");
84
+ if (!v)
85
+ return null;
86
+ if (["full-interactive", "full", "interactive", "full-hands-on", "hands-on", "review-every-step", "guided"].includes(v)) {
87
+ return "full-interactive";
88
+ }
89
+ if (["quick-interactive", "quick", "quick-review", "approve-template", "pick-template", "light"].includes(v)) {
90
+ return "quick-interactive";
91
+ }
92
+ if (["auto-batch", "batch", "batch-review", "full-auto-batch", "auto-with-batch-review", "full-auto-with-batch-review", "autonomous"].includes(v)) {
93
+ return "auto-batch";
94
+ }
95
+ if (["auto-submit", "autosubmit", "auto", "full-auto", "unattended", "headless", "full-autonomous"].includes(v)) {
96
+ return "auto-submit";
97
+ }
98
+ return null;
99
+ }
100
+ // ── the mission folder ───────────────────────────────────────────────────────
101
+ function missionRoot(values) {
102
+ const explicit = values.dir?.trim();
103
+ if (explicit)
104
+ return path.resolve(explicit);
105
+ if (process.env.VIDFARM_CLIPPER_DIR)
106
+ return path.resolve(process.env.VIDFARM_CLIPPER_DIR);
107
+ return path.resolve("CLIPPER");
108
+ }
109
+ function readRunMode(root) {
110
+ // A per-run flag beats the file: `--mode auto-batch` on one command must not
111
+ // silently rewrite the mission's own choice.
112
+ try {
113
+ const parsed = JSON.parse(readFileSync(path.join(root, "run.json"), "utf8"));
114
+ const mode = normalizeRunMode(parsed?.mode);
115
+ if (mode)
116
+ return { mode, isSet: true, note: parsed.note ?? null };
117
+ }
118
+ catch {
119
+ /* not set yet */
120
+ }
121
+ return { mode: DEFAULT_RUN_MODE, isSet: false };
122
+ }
123
+ function resolveMode(root, values) {
124
+ const flag = normalizeRunMode(values.mode);
125
+ if (flag)
126
+ return { mode: flag, isSet: true };
127
+ const env = normalizeRunMode(process.env.VIDFARM_CLIPPER_RUN_MODE);
128
+ if (env)
129
+ return { mode: env, isSet: true };
130
+ return readRunMode(root);
131
+ }
132
+ function tasksDir(root) {
133
+ return path.join(root, "tasks");
134
+ }
135
+ function taskDir(root, taskId) {
136
+ // Task ids come off the wire; keep them from escaping the mission folder.
137
+ const safe = taskId.replace(/[^A-Za-z0-9._-]/g, "_");
138
+ if (!safe || safe === "." || safe === "..")
139
+ throw new Error(`"${taskId}" is not a usable task id.`);
140
+ return path.join(tasksDir(root), safe);
141
+ }
142
+ function statePath(root, taskId) {
143
+ return path.join(taskDir(root, taskId), "state.json");
144
+ }
145
+ function readState(root, taskId) {
146
+ const file = statePath(root, taskId);
147
+ if (!existsSync(file)) {
148
+ throw new Error(`No state for ${taskId}. Start it first: vidfarm clipper-run start ${taskId} --gig <gig-id>`);
149
+ }
150
+ return JSON.parse(readFileSync(file, "utf8"));
151
+ }
152
+ function listStates(root) {
153
+ const dir = tasksDir(root);
154
+ if (!existsSync(dir))
155
+ return [];
156
+ const rows = [];
157
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
158
+ if (!entry.isDirectory())
159
+ continue;
160
+ const file = path.join(dir, entry.name, "state.json");
161
+ if (!existsSync(file))
162
+ continue;
163
+ try {
164
+ rows.push(JSON.parse(readFileSync(file, "utf8")));
165
+ }
166
+ catch {
167
+ /* a half-written state file is not worth crashing the whole loop over */
168
+ }
169
+ }
170
+ return rows.sort((a, b) => a.created_at.localeCompare(b.created_at));
171
+ }
172
+ function writeState(root, state) {
173
+ state.updated_at = new Date().toISOString();
174
+ const dir = taskDir(root, state.task_id);
175
+ mkdirSync(dir, { recursive: true });
176
+ const file = statePath(root, state.task_id);
177
+ writeFileSync(file, `${JSON.stringify(state, null, 2)}\n`, "utf8");
178
+ // The human's list is a VIEW of the states, so regenerate it every time. A
179
+ // hand-maintained review queue goes stale the first time an agent forgets.
180
+ writeReviewQueue(root);
181
+ return file;
182
+ }
183
+ function note(state, event, text) {
184
+ state.history.push({ ts: new Date().toISOString(), event, ...(text ? { note: text } : {}) });
185
+ }
186
+ /** Does this stage stop for a human under this mode? */
187
+ export function isGated(mode, stage) {
188
+ return MODE_GATES[mode].includes(stage);
189
+ }
190
+ /** The stage after `stage`, or `done` at the end. */
191
+ export function nextStage(stage) {
192
+ const index = STAGES.indexOf(stage);
193
+ if (index < 0 || index >= STAGES.length - 1)
194
+ return "done";
195
+ return STAGES[index + 1];
196
+ }
197
+ /**
198
+ * Finish a stage: open a gate if this mode stops here, otherwise move straight
199
+ * on. Every verb below funnels through this, which is why the four modes need no
200
+ * separate code paths.
201
+ */
202
+ function completeStage(state, stage, ask) {
203
+ state.stage = stage;
204
+ if (isGated(state.mode, stage)) {
205
+ state.gate = { stage, ask: (ask ?? "").trim() || DEFAULT_ASK[stage] || "Review this and approve it.", since: new Date().toISOString() };
206
+ note(state, `gate_opened:${stage}`);
207
+ return { gated: true };
208
+ }
209
+ state.gate = null;
210
+ state.stage = nextStage(stage);
211
+ note(state, `auto_advance:${stage}`);
212
+ return { gated: false };
213
+ }
214
+ // ── the human's list ─────────────────────────────────────────────────────────
215
+ function writeReviewQueue(root) {
216
+ const rows = listStates(root).filter((s) => s.gate);
217
+ const lines = [
218
+ "# Review queue",
219
+ "",
220
+ "Regenerated by `vidfarm clipper-run`. One row per task that is WAITING FOR YOU.",
221
+ "Approve: `vidfarm clipper-run approve <task-id>` · changes: `… changes <task-id> --note \"…\"` · drop: `… drop <task-id> --reason \"…\"`",
222
+ ""
223
+ ];
224
+ if (!rows.length) {
225
+ lines.push("_Nothing waiting for you right now._");
226
+ }
227
+ for (const state of rows) {
228
+ lines.push(`## ${state.task_id} — waiting at \`${state.gate.stage}\``);
229
+ lines.push("");
230
+ lines.push(`- buyer machine: ${state.gig_id ?? "?"}${state.machine ? ` (${state.machine})` : ""}`);
231
+ lines.push(`- price: ${state.price === null ? "TBD" : `$${state.price.toFixed(2)}`} · mode: ${state.mode} · waiting since ${state.gate.since}`);
232
+ if (state.title)
233
+ lines.push(`- brief: ${state.title}`);
234
+ if (state.harness)
235
+ lines.push(`- harness: ${state.harness}`);
236
+ if (state.gate.stage === "shortlist" && state.shortlist.length) {
237
+ lines.push("- templates to choose from:");
238
+ for (const row of state.shortlist) {
239
+ lines.push(` - \`${row.template_id}\`${row.why ? ` — ${row.why}` : ""}`);
240
+ }
241
+ lines.push(` - pick one: \`vidfarm clipper-run pick ${state.task_id} <template_id>\``);
242
+ }
243
+ if (state.gate.stage === "plan" && state.plan_path)
244
+ lines.push(`- plan: ${state.plan_path}`);
245
+ if (state.gate.stage === "raws" && state.raws_dir)
246
+ lines.push(`- drop footage here: ${state.raws_dir}`);
247
+ if (state.gate.stage === "cut" && state.cut_path)
248
+ lines.push(`- watch the cut: ${state.cut_path}`);
249
+ lines.push("");
250
+ lines.push(`**${state.gate.ask}**`);
251
+ lines.push("");
252
+ }
253
+ mkdirSync(root, { recursive: true });
254
+ writeFileSync(path.join(root, "REVIEW_QUEUE.md"), `${lines.join("\n")}\n`, "utf8");
255
+ }
256
+ function appendLedger(root, row) {
257
+ mkdirSync(root, { recursive: true });
258
+ appendFileSync(path.join(root, "LEDGER.jsonl"), `${JSON.stringify(row)}\n`, "utf8");
259
+ }
260
+ // ── what to do next ──────────────────────────────────────────────────────────
261
+ /** The one command an agent should run next for a task, given its stage. */
262
+ export function nextAction(state) {
263
+ if (state.gate) {
264
+ return { who: "gigworker", what: `${state.gate.ask} → vidfarm clipper-run approve ${state.task_id}` };
265
+ }
266
+ switch (state.stage) {
267
+ case "claimed":
268
+ return {
269
+ who: "agent",
270
+ what: `Shortlist templates to adapt: vidfarm discover --query "<the buyer's problem, in their words>" --limit 20 --json` +
271
+ `\n then: vidfarm clipper-run shortlist ${state.task_id} --template <id> --why "<why it fits>" (repeat per candidate)`
272
+ };
273
+ case "shortlist":
274
+ return { who: "agent", what: `Choose the template to adapt: vidfarm clipper-run pick ${state.task_id} <template_id>` };
275
+ case "plan":
276
+ return {
277
+ who: "agent",
278
+ what: `Fork it and write the plan, then record it:` +
279
+ `\n vidfarm fork ${state.template_id ?? "<template_id>"} · vidfarm pull <forkId> --dir ./work · vidfarm harness derive <forkId> --out ./work/HARNESS.md` +
280
+ `\n vidfarm clipper-run plan ${state.task_id} --file ./work/STORYBOARD.md`
281
+ };
282
+ case "raws":
283
+ return {
284
+ who: "agent",
285
+ what: `Procure every raw for every beat in ONE pass (buyer's shared folder → their site → public raws → free stock → your own subscription → billed last), then:` +
286
+ `\n vidfarm clipper-run raws ${state.task_id}`
287
+ };
288
+ case "build":
289
+ return {
290
+ who: "agent",
291
+ what: `Build, lint, render locally, QA, then record the two exports:` +
292
+ `\n vidfarm lint · vidfarm render · vidfarm qa ./work${state.harness ? ` --harness ${state.harness}` : ""}` +
293
+ `\n vidfarm clipper-run cut ${state.task_id} --file ./final-watermarked.mp4 --clean-master ./final-clean.mp4`
294
+ };
295
+ case "cut":
296
+ return { who: "agent", what: `Cut is recorded and no gate is open — go to submit.` };
297
+ case "submit":
298
+ return {
299
+ who: "agent",
300
+ what: `Submit it, then close the row:` +
301
+ `\n vidfarm gigs submit ${state.gig_id ?? "<gig-id>"} --task ${state.task_id} --proof <public watermarked url>${state.clean_master ? ` --clean-master ${state.clean_master}` : ""}` +
302
+ `\n vidfarm clipper-run submitted ${state.task_id} --proof <proof-id>`
303
+ };
304
+ default:
305
+ return { who: "agent", what: "Nothing left on this task." };
306
+ }
307
+ }
308
+ // ── output helpers ───────────────────────────────────────────────────────────
309
+ function out(json, value, human) {
310
+ if (json) {
311
+ console.log(JSON.stringify(value, null, 2));
312
+ return;
313
+ }
314
+ human();
315
+ }
316
+ function stageLine(state) {
317
+ const gate = state.gate ? `${YELLOW}WAITING ON THE GIGWORKER${RESET} at ${BOLD}${state.gate.stage}${RESET}` : `stage ${BOLD}${state.stage}${RESET}`;
318
+ const price = state.price === null ? "price TBD" : `$${state.price.toFixed(2)}`;
319
+ return ` ${BOLD}${state.task_id}${RESET} ${gate} ${DIM}${state.mode} · ${price}${state.gig_id ? " · " + state.gig_id : ""}${RESET}`;
320
+ }
321
+ // ── commands ─────────────────────────────────────────────────────────────────
322
+ const MISSION_TEMPLATE = (mode) => `# Clipper mission
323
+
324
+ payout_wallet: 0xYOUR_BASE_L2_ADDRESS # Base L2, receives USDC
325
+ cash_out: GCash via https://officex.short.gy/solana-usdc-gcash
326
+ cost_mode: minimize # profit = price - spend
327
+ run_mode: ${mode} # full-interactive | quick-interactive | auto-batch | auto-submit
328
+ auto_submit: ${mode === "auto-submit"} # only the gigworker may set this true, in words, on purpose
329
+ specialities: # sticker explainers, meme recaptions, greenscreen reaction
330
+ max_tasks_in_flight: 1 # raise only after the gigworker has watched a finished one
331
+ hardware_tier: # weak | capable — measured once, agentic-clipper.md § 3½
332
+ agent_cli: # antigravity | codex | claude-code | …
333
+ cycle_every: 30 minutes
334
+ stop_if: 2 rejections in a row · any gig warning about funds · 3 failed renders · human says stop
335
+ `;
336
+ function cmdInit(root, values) {
337
+ const mode = resolveMode(root, values).mode;
338
+ mkdirSync(tasksDir(root), { recursive: true });
339
+ const created = [];
340
+ const seed = (name, contents) => {
341
+ const file = path.join(root, name);
342
+ // Never clobber a mission the gigworker has already written into.
343
+ if (existsSync(file))
344
+ return;
345
+ writeFileSync(file, contents, "utf8");
346
+ created.push(file);
347
+ };
348
+ seed("MISSION.md", MISSION_TEMPLATE(mode));
349
+ seed("MACHINES.md", "# Machines\n\n| gig_id | buyer | machine type | available_funds | task price | last paid me | verdict |\n|---|---|---|---|---|---|---|\n");
350
+ seed("NOTES.md", "# Notes\n\nWhat each buyer approved, what they rejected and with which tag, prices that moved.\n");
351
+ seed("EARNINGS.md", "# Earnings\n\nsubmitted / approved / PAID / owed, with dates. `approved` is not `paid`.\n");
352
+ seed("LEDGER.jsonl", "");
353
+ writeReviewQueue(root);
354
+ out(Boolean(values.json), { ok: true, root, mode, created }, () => {
355
+ console.log(`${GREEN}✓${RESET} Mission folder ready at ${BOLD}${root}${RESET}`);
356
+ for (const file of created)
357
+ console.log(` ${DIM}${file}${RESET}`);
358
+ console.log("");
359
+ console.log(`${DIM}Run mode: ${BOLD}${mode}${RESET}${DIM} — change it with: vidfarm clipper-run mode <${RUN_MODES.join("|")}>${RESET}`);
360
+ });
361
+ }
362
+ function cmdMode(root, values, positionals) {
363
+ const requested = positionals[0];
364
+ const json = Boolean(values.json);
365
+ if (requested) {
366
+ const mode = normalizeRunMode(requested);
367
+ if (!mode)
368
+ throw new Error(`Unknown run mode "${requested}". Choose one of: ${RUN_MODES.join(", ")}.`);
369
+ const savedAt = new Date().toISOString();
370
+ mkdirSync(root, { recursive: true });
371
+ const payload = { mode, note: values.note ?? null, savedAt };
372
+ const file = path.join(root, "run.json");
373
+ writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
374
+ // MISSION.md is the file a human reads, so keep its two mode lines honest —
375
+ // but only rewrite lines that already exist. This never invents a mission.
376
+ const missionFile = path.join(root, "MISSION.md");
377
+ if (existsSync(missionFile)) {
378
+ const before = readFileSync(missionFile, "utf8");
379
+ const after = before
380
+ .replace(/^run_mode:.*$/m, `run_mode: ${mode}`)
381
+ .replace(/^auto_submit:.*$/m, `auto_submit: ${mode === "auto-submit"}`);
382
+ if (after !== before)
383
+ writeFileSync(missionFile, after, "utf8");
384
+ }
385
+ out(json, { ok: true, run_mode: mode, saved_at: savedAt, file, gates: MODE_GATES[mode] }, () => {
386
+ console.log(`${GREEN}${BOLD}Run mode: ${mode}${RESET}`);
387
+ console.log(`${DIM}${MODE_BLURB[mode]}${RESET}`);
388
+ const gates = MODE_GATES[mode];
389
+ console.log(`${DIM}Stops for the gigworker: ${gates.length ? gates.join(" · ") : "NONE — this loop submits unattended"}${RESET}`);
390
+ if (mode === "auto-submit") {
391
+ console.log(`${YELLOW}Only the gigworker may choose this${RESET}${DIM} — a rejection is scored on the wallet and shown to buyers as a trust score. Revert to auto-batch after any rejection.${RESET}`);
392
+ }
393
+ console.log(`${DIM}Saved to ${file}.${RESET}`);
394
+ });
395
+ return;
396
+ }
397
+ const resolved = resolveMode(root, values);
398
+ out(json, { ok: true, run_mode: resolved.mode, is_set: resolved.isSet, gates: MODE_GATES[resolved.mode], modes: RUN_MODES }, () => {
399
+ console.log(`${BOLD}run mode: ${resolved.mode}${RESET} ${DIM}(${resolved.isSet ? "set" : "NOT set — assuming " + DEFAULT_RUN_MODE}, mission ${root})${RESET}`);
400
+ console.log("");
401
+ for (const mode of RUN_MODES) {
402
+ console.log(` ${BOLD}${mode}${RESET} ${DIM}— stops at: ${MODE_GATES[mode].join(" · ") || "nothing"}${RESET}`);
403
+ console.log(` ${DIM}${MODE_BLURB[mode]}${RESET}`);
404
+ }
405
+ console.log("");
406
+ console.log(`${DIM}Set it: ${BOLD}vidfarm clipper-run mode <${RUN_MODES.join("|")}>${RESET}`);
407
+ if (!resolved.isSet) {
408
+ console.log(`${DIM}Nothing saved yet — ASK the gigworker which one they want before the first task.${RESET}`);
409
+ }
410
+ });
411
+ }
412
+ function numberOrNull(value) {
413
+ const parsed = typeof value === "string" ? Number(value) : typeof value === "number" ? value : NaN;
414
+ return Number.isFinite(parsed) ? parsed : null;
415
+ }
416
+ function cmdStart(root, values, positionals) {
417
+ const taskId = positionals[0];
418
+ if (!taskId)
419
+ throw new Error("clipper-run start needs the task id: vidfarm clipper-run start TASK_01H… --gig GIG_01H…");
420
+ const mode = resolveMode(root, values).mode;
421
+ const now = new Date().toISOString();
422
+ const dir = taskDir(root, taskId);
423
+ mkdirSync(path.join(dir, "raws"), { recursive: true });
424
+ const state = {
425
+ task_id: taskId,
426
+ gig_id: values.gig ?? null,
427
+ machine: values.machine ?? null,
428
+ price: numberOrNull(values.price),
429
+ title: values.title ?? null,
430
+ harness: values.harness ?? null,
431
+ mode,
432
+ stage: "claimed",
433
+ gate: null,
434
+ shortlist: [],
435
+ template_id: null,
436
+ template_picked_by: null,
437
+ plan_path: null,
438
+ raws_dir: path.join(dir, "raws"),
439
+ cut_path: null,
440
+ clean_master: null,
441
+ proof_id: null,
442
+ reviewed_by_gigworker: null,
443
+ history: [{ ts: now, event: "started" }],
444
+ created_at: now,
445
+ updated_at: now
446
+ };
447
+ // The brief is what the subagent actually reads — write the raw payload down
448
+ // rather than paraphrasing it into a chat message that dies with the context.
449
+ const briefFile = values.brief ? String(values.brief) : null;
450
+ if (briefFile) {
451
+ writeFileSync(path.join(dir, "brief.md"), readFileSync(briefFile, "utf8"), "utf8");
452
+ }
453
+ writeState(root, state);
454
+ out(Boolean(values.json), { ok: true, state, next: nextAction(state) }, () => {
455
+ console.log(`${GREEN}✓${RESET} Task ${BOLD}${taskId}${RESET} opened in ${BOLD}${mode}${RESET} ${DIM}(${dir})${RESET}`);
456
+ const next = nextAction(state);
457
+ console.log(` ${DIM}next (${next.who}):${RESET} ${next.what}`);
458
+ });
459
+ }
460
+ function cmdShortlist(root, values, positionals) {
461
+ const state = readState(root, requireTask(positionals[0], "shortlist"));
462
+ const templates = [].concat(values.template ?? []);
463
+ const whys = [].concat(values.why ?? []);
464
+ if (!templates.length) {
465
+ throw new Error('clipper-run shortlist needs at least one --template <id> (repeat it, with a matching --why "<reason>").');
466
+ }
467
+ for (const [index, template] of templates.entries()) {
468
+ state.shortlist.push({ template_id: template, why: whys[index] ?? "" });
469
+ }
470
+ note(state, "shortlisted", templates.join(", "));
471
+ // Shortlist is the one stage that never auto-advances: SOMEBODY still has to
472
+ // choose, and in the two auto modes that somebody is the agent itself. So the
473
+ // task waits at `shortlist` for a `pick` either way — only the gate differs.
474
+ state.stage = "shortlist";
475
+ const gated = isGated(state.mode, "shortlist");
476
+ state.gate = gated
477
+ ? { stage: "shortlist", ask: (values.ask ?? "").trim() || DEFAULT_ASK.shortlist, since: new Date().toISOString() }
478
+ : null;
479
+ if (gated)
480
+ note(state, "gate_opened:shortlist");
481
+ writeState(root, state);
482
+ out(Boolean(values.json), { ok: true, gated, state, next: nextAction(state) }, () => {
483
+ console.log(`${GREEN}✓${RESET} ${state.shortlist.length} template(s) shortlisted for ${BOLD}${state.task_id}${RESET}`);
484
+ for (const row of state.shortlist)
485
+ console.log(` ${BOLD}${row.template_id}${RESET}${row.why ? ` ${DIM}— ${row.why}${RESET}` : ""}`);
486
+ console.log("");
487
+ if (gated) {
488
+ console.log(`${YELLOW}STOP — show these to the gigworker and wait.${RESET} ${DIM}${state.gate.ask}${RESET}`);
489
+ console.log(`${DIM}They answer with: vidfarm clipper-run pick ${state.task_id} <template_id>${RESET}`);
490
+ console.log(`${DIM}Show them the videos first: https://vidfarm.cc/template/<template_id>${RESET}`);
491
+ }
492
+ else {
493
+ console.log(`${DIM}This mode does not stop here — pick one yourself: vidfarm clipper-run pick ${state.task_id} <template_id>${RESET}`);
494
+ }
495
+ });
496
+ }
497
+ function cmdPick(root, values, positionals) {
498
+ const state = readState(root, requireTask(positionals[0], "pick"));
499
+ const flagged = [].concat(values.template ?? [])[0];
500
+ // With no id at all, take the top of the shortlist. That is the auto-mode
501
+ // shape — the agent shortlisted best-first, so "pick" means "take your own
502
+ // first choice" — and it saves a weak model from re-typing an id it just wrote.
503
+ const templateId = positionals[1] ?? flagged ?? state.shortlist[0]?.template_id;
504
+ if (!templateId) {
505
+ throw new Error("clipper-run pick needs a template id (or a shortlist to take the top of): vidfarm clipper-run pick <task-id> template_019…");
506
+ }
507
+ // A pick IS the answer to the shortlist gate, so it closes it. That saves the
508
+ // gigworker from having to both pick and approve.
509
+ const wasGated = state.gate?.stage === "shortlist";
510
+ state.template_id = String(templateId);
511
+ state.template_picked_by = (normalizeWho(values.by) ?? (wasGated ? "human" : "agent"));
512
+ note(state, "template_picked", `${state.template_id} by ${state.template_picked_by}`);
513
+ state.gate = null;
514
+ state.stage = "plan";
515
+ writeState(root, state);
516
+ out(Boolean(values.json), { ok: true, state, next: nextAction(state) }, () => {
517
+ console.log(`${GREEN}✓${RESET} ${BOLD}${state.task_id}${RESET} will adapt ${BOLD}${state.template_id}${RESET} ${DIM}(chosen by the ${state.template_picked_by})${RESET}`);
518
+ const next = nextAction(state);
519
+ console.log(` ${DIM}next (${next.who}):${RESET} ${next.what}`);
520
+ });
521
+ }
522
+ function normalizeWho(raw) {
523
+ const v = String(raw ?? "").trim().toLowerCase();
524
+ if (["human", "gigworker", "me", "user"].includes(v))
525
+ return "human";
526
+ if (["agent", "ai", "auto"].includes(v))
527
+ return "agent";
528
+ return null;
529
+ }
530
+ function cmdPlan(root, values, positionals) {
531
+ const state = readState(root, requireTask(positionals[0], "plan"));
532
+ if (values.file)
533
+ state.plan_path = path.resolve(String(values.file));
534
+ if (!state.plan_path) {
535
+ throw new Error('clipper-run plan needs --file <path to the written plan> (STORYBOARD.md, brief.md — whatever you wrote).');
536
+ }
537
+ note(state, "planned", state.plan_path);
538
+ const { gated } = completeStage(state, "plan", values.ask);
539
+ writeState(root, state);
540
+ out(Boolean(values.json), { ok: true, gated, state, next: nextAction(state) }, () => {
541
+ console.log(`${GREEN}✓${RESET} Plan recorded for ${BOLD}${state.task_id}${RESET}: ${state.plan_path}`);
542
+ if (gated) {
543
+ console.log(`${YELLOW}STOP — hand the plan to the gigworker and wait.${RESET} ${DIM}${state.gate.ask}${RESET}`);
544
+ console.log(`${DIM}They answer: vidfarm clipper-run approve ${state.task_id} · vidfarm clipper-run changes ${state.task_id} --note "…"${RESET}`);
545
+ }
546
+ else {
547
+ const next = nextAction(state);
548
+ console.log(` ${DIM}next (${next.who}):${RESET} ${next.what}`);
549
+ }
550
+ });
551
+ }
552
+ function cmdRaws(root, values, positionals) {
553
+ const state = readState(root, requireTask(positionals[0], "raws"));
554
+ const dir = state.raws_dir ?? path.join(taskDir(root, state.task_id), "raws");
555
+ mkdirSync(dir, { recursive: true });
556
+ state.raws_dir = dir;
557
+ const have = readdirSync(dir).filter((f) => !f.startsWith("."));
558
+ note(state, "raws_folder", `${have.length} file(s)`);
559
+ const { gated } = completeStage(state, "raws", values.ask);
560
+ writeState(root, state);
561
+ out(Boolean(values.json), { ok: true, gated, raws_dir: dir, files: have, state, next: nextAction(state) }, () => {
562
+ console.log(`${GREEN}✓${RESET} Raws folder for ${BOLD}${state.task_id}${RESET}:`);
563
+ console.log(` ${BOLD}${dir}${RESET} ${DIM}${have.length} file(s)${RESET}`);
564
+ if (gated) {
565
+ console.log("");
566
+ console.log(`${YELLOW}STOP — offer the folder to the gigworker.${RESET} ${DIM}${state.gate.ask}${RESET}`);
567
+ console.log(`${DIM}A human eye picks better clips than any keyword scan, and it costs $0. But it is OPTIONAL —${RESET}`);
568
+ console.log(`${DIM}if they say "go", approve it yourself on their word: vidfarm clipper-run approve ${state.task_id} --note "gigworker said go"${RESET}`);
569
+ }
570
+ else {
571
+ const next = nextAction(state);
572
+ console.log(` ${DIM}next (${next.who}):${RESET} ${next.what}`);
573
+ }
574
+ });
575
+ }
576
+ function cmdCut(root, values, positionals) {
577
+ const state = readState(root, requireTask(positionals[0], "cut"));
578
+ if (values.file)
579
+ state.cut_path = path.resolve(String(values.file));
580
+ if (values["clean-master"])
581
+ state.clean_master = path.resolve(String(values["clean-master"]));
582
+ if (!state.cut_path)
583
+ throw new Error("clipper-run cut needs --file <path to the WATERMARKED cut>.");
584
+ note(state, "cut_ready", state.cut_path);
585
+ const { gated } = completeStage(state, "cut", values.ask);
586
+ writeState(root, state);
587
+ out(Boolean(values.json), { ok: true, gated, state, next: nextAction(state) }, () => {
588
+ console.log(`${GREEN}✓${RESET} Cut recorded for ${BOLD}${state.task_id}${RESET}: ${state.cut_path}`);
589
+ if (!state.clean_master) {
590
+ console.log(` ${YELLOW}No --clean-master given${RESET}${DIM} — the watermarked cut is what the buyer sees; the clean master goes in the proof's private note.${RESET}`);
591
+ }
592
+ if (gated) {
593
+ console.log("");
594
+ console.log(`${YELLOW}STOP — the gigworker watches this before it is submitted.${RESET}`);
595
+ console.log(`${DIM}${state.gate.ask}${RESET}`);
596
+ console.log(`${DIM}Play it: vidfarm serve · fast look: vidfarm stills ${state.cut_path}${RESET}`);
597
+ console.log(`${DIM}They answer: vidfarm clipper-run approve ${state.task_id} · changes … --note "…" · drop … --reason "…"${RESET}`);
598
+ }
599
+ else {
600
+ console.log(` ${YELLOW}auto-submit${RESET}${DIM} — no human watches this one. A rejection lands on the public trust score.${RESET}`);
601
+ const next = nextAction(state);
602
+ console.log(` ${DIM}next (${next.who}):${RESET} ${next.what}`);
603
+ }
604
+ });
605
+ }
606
+ function cmdApprove(root, values, positionals) {
607
+ const state = readState(root, requireTask(positionals[0], "approve"));
608
+ if (!state.gate)
609
+ throw new Error(`${state.task_id} is not waiting for anybody — it is at stage "${state.stage}". Run: vidfarm clipper-run status ${state.task_id}`);
610
+ const stage = state.gate.stage;
611
+ const at = new Date().toISOString();
612
+ note(state, `approved:${stage}`, values.note ?? null);
613
+ // Only the CUT gate is the "a human watched the video" record — that is the
614
+ // one the ledger and the buyer's trust score care about.
615
+ if (stage === "cut")
616
+ state.reviewed_by_gigworker = at;
617
+ state.gate = null;
618
+ state.stage = nextStage(stage);
619
+ writeState(root, state);
620
+ out(Boolean(values.json), { ok: true, approved: stage, state, next: nextAction(state) }, () => {
621
+ console.log(`${GREEN}✓${RESET} ${BOLD}${state.task_id}${RESET} approved at ${BOLD}${stage}${RESET} → stage ${BOLD}${state.stage}${RESET}`);
622
+ const next = nextAction(state);
623
+ console.log(` ${DIM}next (${next.who}):${RESET} ${next.what}`);
624
+ });
625
+ }
626
+ function cmdChanges(root, values, positionals) {
627
+ const state = readState(root, requireTask(positionals[0], "changes"));
628
+ const text = values.note?.trim();
629
+ if (!text)
630
+ throw new Error('clipper-run changes needs --note "<what to fix>" — that note is the whole point.');
631
+ const stage = state.gate?.stage ?? state.stage;
632
+ note(state, `changes_requested:${stage}`, text);
633
+ state.gate = null;
634
+ // Send it back to the stage that produced the thing being rejected, so the
635
+ // agent redoes THAT step rather than starting the task over. "not these
636
+ // templates" means shortlist again, which is the stage BEFORE the list.
637
+ state.stage = stage === "cut" ? "build" : stage === "shortlist" ? "claimed" : stage;
638
+ writeState(root, state);
639
+ out(Boolean(values.json), { ok: true, state, next: nextAction(state) }, () => {
640
+ console.log(`${GREEN}✓${RESET} Changes recorded on ${BOLD}${state.task_id}${RESET} at ${BOLD}${stage}${RESET} → back to ${BOLD}${state.stage}${RESET}`);
641
+ console.log(` ${DIM}${text}${RESET}`);
642
+ console.log(` ${DIM}Redo that step with a FRESH subagent and the note above, then return to the same gate.${RESET}`);
643
+ });
644
+ }
645
+ function cmdDrop(root, values, positionals) {
646
+ const state = readState(root, requireTask(positionals[0], "drop"));
647
+ const reason = values.reason?.trim() || "no reason given";
648
+ note(state, "discarded", reason);
649
+ state.gate = null;
650
+ state.stage = "discarded";
651
+ writeState(root, state);
652
+ appendLedger(root, {
653
+ ts: new Date().toISOString(),
654
+ gig_id: state.gig_id,
655
+ task_id: state.task_id,
656
+ template_id: state.template_id,
657
+ run_mode: state.mode,
658
+ status: "discarded",
659
+ reason
660
+ });
661
+ out(Boolean(values.json), { ok: true, state }, () => {
662
+ console.log(`${GREEN}✓${RESET} Dropped ${BOLD}${state.task_id}${RESET} — ${reason}`);
663
+ console.log(` ${DIM}Logged to LEDGER.jsonl. On Custom Requests, release it or let it expire — never submit a cut the gigworker rejected.${RESET}`);
664
+ });
665
+ }
666
+ function cmdSubmitted(root, values, positionals) {
667
+ const state = readState(root, requireTask(positionals[0], "submitted"));
668
+ state.proof_id = values.proof ?? null;
669
+ note(state, "submitted", state.proof_id);
670
+ state.gate = null;
671
+ state.stage = "done";
672
+ writeState(root, state);
673
+ appendLedger(root, {
674
+ ts: new Date().toISOString(),
675
+ gig_id: state.gig_id,
676
+ task_id: state.task_id,
677
+ proof_id: state.proof_id,
678
+ template_id: state.template_id,
679
+ harness: state.harness,
680
+ run_mode: state.mode,
681
+ locked_price: state.price,
682
+ status: "submitted",
683
+ reviewed_by_gigworker: state.reviewed_by_gigworker ?? (state.mode === "auto-submit" ? "auto_submit" : null),
684
+ paid_out_at: null
685
+ });
686
+ out(Boolean(values.json), { ok: true, state }, () => {
687
+ console.log(`${GREEN}✓${RESET} ${BOLD}${state.task_id}${RESET} closed as submitted${state.proof_id ? ` ${DIM}(${state.proof_id})${RESET}` : ""}`);
688
+ console.log(` ${DIM}Ledger row appended. approved is not paid — reconcile with: vidfarm gigs proof ${state.gig_id ?? "<gig>"} ${state.proof_id ?? "<proof-id>"}${RESET}`);
689
+ });
690
+ }
691
+ function cmdStatus(root, values, positionals) {
692
+ const json = Boolean(values.json);
693
+ if (positionals[0]) {
694
+ const state = readState(root, positionals[0]);
695
+ out(json, { ok: true, state, next: nextAction(state) }, () => {
696
+ console.log(stageLine(state));
697
+ if (state.template_id)
698
+ console.log(` ${DIM}template ${state.template_id} (${state.template_picked_by})${RESET}`);
699
+ if (state.plan_path)
700
+ console.log(` ${DIM}plan ${state.plan_path}${RESET}`);
701
+ if (state.cut_path)
702
+ console.log(` ${DIM}cut ${state.cut_path}${RESET}`);
703
+ const next = nextAction(state);
704
+ console.log(` ${DIM}next (${next.who}):${RESET} ${next.what}`);
705
+ });
706
+ return;
707
+ }
708
+ const rows = listStates(root);
709
+ const open = rows.filter((s) => s.stage !== "done" && s.stage !== "discarded");
710
+ out(json, { ok: true, root, run_mode: resolveMode(root, values).mode, tasks: rows }, () => {
711
+ const mode = resolveMode(root, values);
712
+ console.log(`${BOLD}run mode: ${mode.mode}${RESET} ${DIM}(${mode.isSet ? "set" : "not set"}) · ${root}${RESET}`);
713
+ if (!rows.length) {
714
+ console.log(`${DIM}No tasks yet. Claim one, then: vidfarm clipper-run start <task-id> --gig <gig-id>${RESET}`);
715
+ return;
716
+ }
717
+ for (const state of open)
718
+ console.log(stageLine(state));
719
+ const closed = rows.length - open.length;
720
+ if (closed)
721
+ console.log(`${DIM} …and ${closed} closed task(s).${RESET}`);
722
+ const waiting = open.filter((s) => s.gate).length;
723
+ console.log("");
724
+ console.log(waiting
725
+ ? `${YELLOW}${waiting} task(s) waiting on the gigworker${RESET}${DIM} — the list is in ${path.join(root, "REVIEW_QUEUE.md")}${RESET}`
726
+ : `${DIM}Nothing waiting on the gigworker.${RESET}`);
727
+ });
728
+ }
729
+ function cmdNext(root, values) {
730
+ const rows = listStates(root).filter((s) => s.stage !== "done" && s.stage !== "discarded");
731
+ const waiting = rows.filter((s) => s.gate);
732
+ const actionable = rows.filter((s) => !s.gate);
733
+ out(Boolean(values.json), {
734
+ ok: true,
735
+ run_mode: resolveMode(root, values).mode,
736
+ waiting_on_gigworker: waiting.map((s) => ({ task_id: s.task_id, stage: s.gate.stage, ask: s.gate.ask })),
737
+ next: actionable.map((s) => ({ task_id: s.task_id, stage: s.stage, ...nextAction(s) }))
738
+ }, () => {
739
+ if (!rows.length) {
740
+ console.log(`${DIM}No open tasks. Find work: vidfarm gigs earn → vidfarm gigs claim <gig-id> → vidfarm clipper-run start <task-id> --gig <gig-id>${RESET}`);
741
+ return;
742
+ }
743
+ if (actionable.length) {
744
+ console.log(`${BOLD}Do this now${RESET}`);
745
+ for (const state of actionable) {
746
+ const next = nextAction(state);
747
+ console.log(` ${BOLD}${state.task_id}${RESET} ${DIM}(${state.stage})${RESET}`);
748
+ console.log(` ${next.what}`);
749
+ }
750
+ }
751
+ else {
752
+ console.log(`${DIM}Nothing for you to do — every open task is waiting on the gigworker.${RESET}`);
753
+ }
754
+ if (waiting.length) {
755
+ console.log("");
756
+ console.log(`${YELLOW}Waiting on the gigworker — do NOT work around these${RESET}`);
757
+ for (const state of waiting) {
758
+ console.log(` ${BOLD}${state.task_id}${RESET} ${DIM}at ${state.gate.stage} since ${state.gate.since}${RESET}`);
759
+ console.log(` ${DIM}${state.gate.ask}${RESET}`);
760
+ }
761
+ }
762
+ });
763
+ }
764
+ function cmdReview(root, values) {
765
+ const rows = listStates(root).filter((s) => s.gate);
766
+ writeReviewQueue(root);
767
+ out(Boolean(values.json), { ok: true, waiting: rows }, () => {
768
+ if (!rows.length) {
769
+ console.log(`${DIM}Nothing waiting for the gigworker.${RESET}`);
770
+ return;
771
+ }
772
+ console.log(`${BOLD}${rows.length} task(s) waiting for you${RESET} ${DIM}(also written to ${path.join(root, "REVIEW_QUEUE.md")})${RESET}`);
773
+ for (const state of rows) {
774
+ console.log("");
775
+ console.log(` ${BOLD}${state.task_id}${RESET} ${DIM}${state.gate.stage} · ${state.mode}${state.price === null ? "" : ` · $${state.price.toFixed(2)}`}${RESET}`);
776
+ if (state.gate.stage === "shortlist") {
777
+ for (const row of state.shortlist)
778
+ console.log(` ${BOLD}${row.template_id}${RESET}${row.why ? ` ${DIM}— ${row.why}${RESET}` : ""}`);
779
+ }
780
+ if (state.gate.stage === "plan" && state.plan_path)
781
+ console.log(` plan: ${state.plan_path}`);
782
+ if (state.gate.stage === "raws" && state.raws_dir)
783
+ console.log(` drop footage in: ${state.raws_dir}`);
784
+ if (state.gate.stage === "cut" && state.cut_path)
785
+ console.log(` watch: ${state.cut_path}`);
786
+ console.log(` ${DIM}${state.gate.ask}${RESET}`);
787
+ console.log(` ${DIM}vidfarm clipper-run approve ${state.task_id}${state.gate.stage === "shortlist" ? ` (or: pick ${state.task_id} <template_id>)` : ""}${RESET}`);
788
+ }
789
+ });
790
+ }
791
+ /**
792
+ * The pre-submit check. Exits non-zero while a human gate is open, so a loop
793
+ * script can be written as `vidfarm clipper-run gate $TASK && vidfarm gigs
794
+ * submit …` and be physically unable to send an unreviewed cut.
795
+ */
796
+ function cmdGate(root, values, positionals) {
797
+ const state = readState(root, requireTask(positionals[0], "gate"));
798
+ const blocked = Boolean(state.gate) || (state.stage !== "submit" && state.stage !== "done");
799
+ out(Boolean(values.json), {
800
+ ok: !blocked,
801
+ task_id: state.task_id,
802
+ stage: state.stage,
803
+ gate: state.gate,
804
+ may_submit: !blocked,
805
+ reviewed_by_gigworker: state.reviewed_by_gigworker
806
+ }, () => {
807
+ if (!blocked) {
808
+ console.log(`${GREEN}✓ CLEARED TO SUBMIT${RESET} ${BOLD}${state.task_id}${RESET}${state.reviewed_by_gigworker ? ` ${DIM}(gigworker approved the cut at ${state.reviewed_by_gigworker})${RESET}` : ` ${DIM}(auto-submit — no human watched this)${RESET}`}`);
809
+ return;
810
+ }
811
+ console.log(`${YELLOW}HOLD${RESET} ${BOLD}${state.task_id}${RESET} ${DIM}is at ${state.stage}${state.gate ? ` and waiting on the gigworker (${state.gate.stage})` : ""}${RESET}`);
812
+ if (state.gate)
813
+ console.log(` ${DIM}${state.gate.ask}${RESET}`);
814
+ });
815
+ if (blocked)
816
+ process.exitCode = 2;
817
+ }
818
+ function cmdNote(root, values, positionals) {
819
+ const state = readState(root, requireTask(positionals[0], "note"));
820
+ const text = positionals.slice(1).join(" ").trim() || String(values.note ?? "").trim();
821
+ if (!text)
822
+ throw new Error('clipper-run note needs some text: vidfarm clipper-run note <task-id> "buyer wants the logo bigger"');
823
+ note(state, "note", text);
824
+ writeState(root, state);
825
+ out(Boolean(values.json), { ok: true, state }, () => {
826
+ console.log(`${GREEN}✓${RESET} Noted on ${BOLD}${state.task_id}${RESET}.`);
827
+ });
828
+ }
829
+ function requireTask(raw, verb) {
830
+ if (!raw)
831
+ throw new Error(`clipper-run ${verb} needs a task id: vidfarm clipper-run ${verb} TASK_01H…`);
832
+ return raw;
833
+ }
834
+ // ── help + dispatch ──────────────────────────────────────────────────────────
835
+ export const CLIPPER_RUN_HELP = `vidfarm clipper-run — how much of the gigworker's attention each task costs, and where the loop is up to
836
+
837
+ THE FOUR RUN MODES (they differ ONLY in which stages stop for the human)
838
+ full-interactive stops at shortlist · plan · raws · cut best quality on a weak model
839
+ quick-interactive stops at shortlist · cut the gigworker approves ONE template, then watches the cut
840
+ auto-batch stops at cut builds everything unattended, submits nothing until reviewed
841
+ auto-submit stops nowhere fully unattended. Opt-in, in the gigworker's own words.
842
+
843
+ clipper-run mode [<mode>] Show or set the run mode (writes CLIPPER/run.json + MISSION.md)
844
+ clipper-run init Create the mission folder (MISSION.md, MACHINES.md, LEDGER.jsonl, …)
845
+
846
+ ONE TASK, START TO FINISH
847
+ clipper-run start <task-id> --gig <id> --price <usd> --machine <slug> --title "<brief>"
848
+ --harness <url> --brief <file.md>
849
+ clipper-run shortlist <task-id> --template <id> --why "<why it fits>" (repeat per candidate)
850
+ clipper-run pick <task-id> <template-id> The choice. Closes the shortlist gate.
851
+ clipper-run plan <task-id> --file ./work/STORYBOARD.md
852
+ clipper-run raws <task-id> Print (and create) this task's local raws folder
853
+ clipper-run cut <task-id> --file ./final-watermarked.mp4 --clean-master ./final-clean.mp4
854
+ clipper-run gate <task-id> MAY I SUBMIT? exits non-zero while a human gate is open
855
+ clipper-run submitted <task-id> --proof <proof-id> Close the row + append the ledger
856
+
857
+ THE GIGWORKER ANSWERS
858
+ clipper-run approve <task-id> [--note "<text>"]
859
+ clipper-run changes <task-id> --note "<what to fix>"
860
+ clipper-run drop <task-id> --reason "<why>"
861
+
862
+ WHERE AM I
863
+ clipper-run next THE ONE COMMAND — what to do right now, per open task
864
+ clipper-run status [<task-id>] Every task, its stage, and who it is waiting on
865
+ clipper-run review The batch review list (also written to REVIEW_QUEUE.md)
866
+ clipper-run note <task-id> "<text>"
867
+
868
+ Common: --dir <mission folder, default ./CLIPPER> · --mode <run mode, for this call only> · --json
869
+ State lives in files on purpose: a weak model loses the thread, a file does not.
870
+ The loop this serves: https://vidfarm.cc/agentic-clipper.md`;
871
+ function options() {
872
+ return {
873
+ dir: { type: "string" },
874
+ mode: { type: "string" },
875
+ json: { type: "boolean" },
876
+ gig: { type: "string" },
877
+ machine: { type: "string" },
878
+ price: { type: "string" },
879
+ title: { type: "string" },
880
+ harness: { type: "string" },
881
+ brief: { type: "string" },
882
+ template: { type: "string", multiple: true },
883
+ why: { type: "string", multiple: true },
884
+ file: { type: "string" },
885
+ "clean-master": { type: "string" },
886
+ proof: { type: "string" },
887
+ note: { type: "string" },
888
+ reason: { type: "string" },
889
+ ask: { type: "string" },
890
+ by: { type: "string" }
891
+ };
892
+ }
893
+ export async function runClipperRunCommand(argv) {
894
+ const sub = (argv[0] ?? "help").toLowerCase();
895
+ if (["help", "--help", "-h", ""].includes(sub)) {
896
+ console.log(CLIPPER_RUN_HELP);
897
+ return;
898
+ }
899
+ const parsed = parseArgs({ args: argv.slice(1), allowPositionals: true, options: options() });
900
+ const values = parsed.values;
901
+ const rest = parsed.positionals;
902
+ const root = missionRoot(values);
903
+ switch (sub) {
904
+ case "init": return cmdInit(root, values);
905
+ case "mode":
906
+ case "run-mode": return cmdMode(root, values, rest);
907
+ case "start":
908
+ case "open": return cmdStart(root, values, rest);
909
+ case "shortlist":
910
+ case "suggest": return cmdShortlist(root, values, rest);
911
+ case "pick":
912
+ case "choose": return cmdPick(root, values, rest);
913
+ case "plan": return cmdPlan(root, values, rest);
914
+ case "raws":
915
+ case "footage": return cmdRaws(root, values, rest);
916
+ case "cut":
917
+ case "built": return cmdCut(root, values, rest);
918
+ case "approve":
919
+ case "ok": return cmdApprove(root, values, rest);
920
+ case "changes":
921
+ case "revise": return cmdChanges(root, values, rest);
922
+ case "drop":
923
+ case "discard": return cmdDrop(root, values, rest);
924
+ case "submitted":
925
+ case "sent": return cmdSubmitted(root, values, rest);
926
+ case "status":
927
+ case "state": return cmdStatus(root, values, rest);
928
+ case "next":
929
+ case "what-now": return cmdNext(root, values);
930
+ case "review":
931
+ case "queue": return cmdReview(root, values);
932
+ case "gate":
933
+ case "may-i-submit": return cmdGate(root, values, rest);
934
+ case "note": return cmdNote(root, values, rest);
935
+ default:
936
+ console.error(`Unknown clipper-run subcommand: ${sub}\n`);
937
+ console.log(CLIPPER_RUN_HELP);
938
+ process.exitCode = 1;
939
+ }
940
+ }
941
+ //# sourceMappingURL=clipper-run.js.map