wholestack 0.5.6 → 0.5.7

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/dist/cli.js CHANGED
@@ -7,17 +7,35 @@ import {
7
7
  PROPERTY_KINDS,
8
8
  Permissions,
9
9
  Session,
10
+ SkillRegistry,
10
11
  announcePlanMode,
12
+ apiJson,
11
13
  banner,
14
+ buildEscalateTool,
15
+ buildEyesTool,
16
+ buildOpsTools,
17
+ buildSkillTools,
12
18
  buildTools,
19
+ buildVerifyTools,
13
20
  buildWebTools,
14
21
  c,
15
22
  collectLocalFiles,
23
+ deployBuild,
24
+ drawCanvas,
25
+ escalationBus,
26
+ fetchGithubInstallations,
27
+ fetchGithubRepos,
28
+ generateLegal,
29
+ getBusiness,
30
+ getLaunchKit,
31
+ inspectUrl,
16
32
  isPermissionMode,
17
33
  killRunningApps,
18
34
  line,
35
+ listBusinesses,
19
36
  listProjects,
20
37
  loadHookFiles,
38
+ loadLearnedMemory,
21
39
  loadMcpTools,
22
40
  loadPlugins,
23
41
  loadProjectMemory,
@@ -26,26 +44,42 @@ import {
26
44
  modelContextWindow,
27
45
  modelLabel,
28
46
  openBuildInEditor,
47
+ pickBootModel,
48
+ pickGithubRepoInteractive,
49
+ promoteBuild,
50
+ proveLocalRepo,
29
51
  pullWorkingTree,
30
52
  pushWorkingTree,
53
+ readMetrics,
31
54
  readProjectLink,
55
+ readSourceFile,
56
+ rememberLesson,
57
+ renderGtmPlanLines,
58
+ renderLaunchKitLines,
59
+ renderMetricsLines,
32
60
  resolveModel,
33
61
  resolveModelKey,
62
+ rollbackDeployment,
34
63
  runProver,
64
+ runRepoAuditStream,
35
65
  saveKey,
66
+ searchLessons,
36
67
  stateDir,
37
68
  statusLine,
38
69
  supportsThinking,
39
70
  tasks,
71
+ toolLine,
40
72
  userBox,
41
73
  visionCapable,
74
+ waitlistCount,
75
+ webBaseUrl,
42
76
  writeProjectLink,
43
77
  writeWorkingTree
44
- } from "./chunk-K6MSDQJT.js";
78
+ } from "./chunk-TLYCOIEA.js";
45
79
 
46
80
  // src/cli.ts
47
81
  import { createInterface } from "readline/promises";
48
- import { stdin, stdout, argv, exit, cwd } from "process";
82
+ import { stdin, stdout as stdout3, argv, exit, cwd as cwd2 } from "process";
49
83
 
50
84
  // src/cli-login.ts
51
85
  import { createServer } from "http";
@@ -298,7 +332,7 @@ async function renderTree(absDir, relDir) {
298
332
  return `${relDir || "."}/ (directory tree)
299
333
  ${lines.join("\n")}`;
300
334
  }
301
- async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET) {
335
+ async function expandMentions(message, cwd3, tokenBudget = DEFAULT_TOKEN_BUDGET) {
302
336
  const mentions = extractMentions(message);
303
337
  if (mentions.length === 0) return { context: "", notice: null };
304
338
  const blocks = [];
@@ -308,7 +342,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
308
342
  for (const mention of mentions) {
309
343
  if (spent >= tokenBudget) break;
310
344
  const raw = mention.startsWith("~/") ? join(process.env.HOME ?? "", mention.slice(2)) : mention;
311
- const direct = isAbsolute(raw) ? raw : resolve(cwd2, raw);
345
+ const direct = isAbsolute(raw) ? raw : resolve(cwd3, raw);
312
346
  let isDir = false;
313
347
  try {
314
348
  isDir = (await stat(direct)).isDirectory();
@@ -316,7 +350,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
316
350
  isDir = false;
317
351
  }
318
352
  if (isDir) {
319
- const rel = relative(cwd2, direct);
353
+ const rel = relative(cwd3, direct);
320
354
  const tree = await renderTree(direct, rel);
321
355
  const cost = estTokens(tree);
322
356
  if (spent + cost <= tokenBudget) {
@@ -329,7 +363,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
329
363
  let files = [];
330
364
  try {
331
365
  files = await fg(raw, {
332
- cwd: cwd2,
366
+ cwd: cwd3,
333
367
  ignore: IGNORE,
334
368
  onlyFiles: true,
335
369
  dot: true,
@@ -341,7 +375,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
341
375
  }
342
376
  if (files.length === 0) {
343
377
  try {
344
- if ((await stat(direct)).isFile()) files = [relative(cwd2, direct)];
378
+ if ((await stat(direct)).isFile()) files = [relative(cwd3, direct)];
345
379
  } catch {
346
380
  }
347
381
  }
@@ -352,7 +386,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
352
386
  seen.add(rel);
353
387
  let buf;
354
388
  try {
355
- buf = await readFile2(resolve(cwd2, rel), "utf8");
389
+ buf = await readFile2(resolve(cwd3, rel), "utf8");
356
390
  } catch {
357
391
  continue;
358
392
  }
@@ -432,11 +466,528 @@ function buildSubagentTool(deps) {
432
466
  };
433
467
  }
434
468
 
469
+ // src/eyes-cli.ts
470
+ function flagValue(args, flag) {
471
+ const i = args.indexOf(flag);
472
+ return i >= 0 && i + 1 < args.length ? args[i + 1] : void 0;
473
+ }
474
+ function printFinding(f) {
475
+ line();
476
+ line(f.loaded ? c.green(" \u2713 loaded") + c.dim(` ${f.url}`) : c.red(" \u2717 not healthy") + c.dim(` ${f.url}`));
477
+ if (f.httpStatus !== null) line(c.dim(` HTTP ${f.httpStatus}`) + (f.title ? c.dim(` \xB7 ${f.title}`) : ""));
478
+ if (f.pageErrors.length) {
479
+ line(c.red(` page errors (${f.pageErrors.length}):`));
480
+ for (const e of f.pageErrors.slice(0, 8)) line(" " + c.dim(e));
481
+ }
482
+ if (f.consoleErrors.length) {
483
+ line(c.yellow(` console errors (${f.consoleErrors.length}):`));
484
+ for (const e of f.consoleErrors.slice(0, 8)) line(" " + c.dim(e));
485
+ }
486
+ if (f.failedRequests.length) {
487
+ line(c.yellow(` failed requests (${f.failedRequests.length}):`));
488
+ for (const r of f.failedRequests.slice(0, 8)) line(" " + c.dim(`${r.method} ${r.url}`));
489
+ }
490
+ if (f.screenshot) line(c.cyan(` screenshot \u2192 ${f.screenshot}`));
491
+ line(c.dim(" " + f.verdict));
492
+ }
493
+ async function runEyesSubcommand(raw) {
494
+ if (raw[0] !== "eyes" && raw[0] !== "see") return null;
495
+ const rest = raw.slice(1);
496
+ const wantsJson = rest.includes("--json");
497
+ const noShot = rest.includes("--no-screenshot");
498
+ const shotPath = flagValue(rest, "--out");
499
+ const url = rest.find((t) => !t.startsWith("-"));
500
+ if (!url) {
501
+ if (wantsJson) {
502
+ process.stdout.write(JSON.stringify({ ok: false, error: "missing URL" }) + "\n");
503
+ } else {
504
+ line(c.red(" usage: wholestack eyes <url> [--json] [--no-screenshot] [--out file.png]"));
505
+ }
506
+ return 1;
507
+ }
508
+ const res = await inspectUrl({
509
+ cwd: process.cwd(),
510
+ url,
511
+ capture: !noShot,
512
+ screenshotPath: shotPath
513
+ });
514
+ if (!("ok" in res) || res.ok !== true) {
515
+ const error = res.error;
516
+ if (wantsJson) {
517
+ process.stdout.write(JSON.stringify({ ok: false, error }) + "\n");
518
+ } else {
519
+ const lines = error.split("\n");
520
+ line(c.red(" \u2717 " + lines[0]));
521
+ for (const l of lines.slice(1)) line(c.dim(" " + l));
522
+ }
523
+ return 1;
524
+ }
525
+ if (wantsJson) {
526
+ const { dataUrl, ...rest2 } = res;
527
+ void dataUrl;
528
+ process.stdout.write(JSON.stringify(rest2) + "\n");
529
+ return res.loaded ? 0 : 1;
530
+ }
531
+ printFinding(res);
532
+ return res.loaded ? 0 : 1;
533
+ }
534
+
535
+ // src/workers.ts
536
+ import { spawn as spawn2 } from "child_process";
537
+ import { mkdtemp, rm as rm2 } from "fs/promises";
538
+ import { tmpdir } from "os";
539
+ import { join as join2 } from "path";
540
+ import { generateText as generateText2, stepCountIs as stepCountIs2, tool as tool2 } from "ai";
541
+ import { z as z2 } from "zod";
542
+ var WORKER_TOOLS = [
543
+ "read_file",
544
+ "write_file",
545
+ "edit_file",
546
+ "multi_edit",
547
+ "list_dir",
548
+ "glob",
549
+ "grep",
550
+ "run_command",
551
+ "todo_write",
552
+ "todo_read"
553
+ ];
554
+ function workerToolset(worktreeCtx) {
555
+ const all = buildTools(worktreeCtx);
556
+ const out = {};
557
+ for (const k of WORKER_TOOLS) if (all[k]) out[k] = all[k];
558
+ return out;
559
+ }
560
+ var WORKER_SYSTEM = "You are an autonomous engineer spawned by a lead engineer to complete ONE focused, self-contained slice of a larger task. You have a full coding toolset (read_file, write_file, edit_file, multi_edit, list_dir, glob, grep, run_command) and you are working in your OWN isolated git worktree \u2014 your edits cannot affect anyone else, so edit freely. Approach: (1) explore the relevant files, (2) make the change, (3) verify it (build/test/typecheck where it makes sense), (4) report. Stay strictly within your assigned slice \u2014 do not refactor unrelated code. When done, return a tight summary: what you changed, which files, and how you verified it.";
561
+ function git(args, cwd3, timeoutMs = 3e4) {
562
+ return new Promise((res) => {
563
+ const child = spawn2("git", args, { cwd: cwd3, shell: false });
564
+ let out = "";
565
+ const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
566
+ const sink = (b) => {
567
+ out += b.toString();
568
+ };
569
+ child.stdout.on("data", sink);
570
+ child.stderr.on("data", sink);
571
+ child.on("error", (e) => {
572
+ clearTimeout(timer);
573
+ res({ code: 1, out: e.message });
574
+ });
575
+ child.on("close", (code) => {
576
+ clearTimeout(timer);
577
+ res({ code: code ?? 1, out });
578
+ });
579
+ });
580
+ }
581
+ async function repoRoot(dir) {
582
+ const r = await git(["rev-parse", "--show-toplevel"], dir);
583
+ if (r.code !== 0) return null;
584
+ const top = r.out.trim();
585
+ return top || null;
586
+ }
587
+ function slug(label) {
588
+ const base2 = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 24) || "worker";
589
+ const rand = Math.random().toString(36).slice(2, 8);
590
+ return `${base2}-${rand}`;
591
+ }
592
+ var MAX_DIFF_CHARS = 24e3;
593
+ function clampDiff(s) {
594
+ if (s.length <= MAX_DIFF_CHARS) return s;
595
+ return s.slice(0, MAX_DIFF_CHARS) + `
596
+ \u2026 [diff truncated at ${MAX_DIFF_CHARS} chars \u2014 ${s.length} total]`;
597
+ }
598
+ async function runOneWorker(deps, root, label, task, maxSteps, timeoutMs) {
599
+ const name = slug(label);
600
+ const branch = `zeta-worker/${name}`;
601
+ let worktree = null;
602
+ try {
603
+ const parent = await mkdtemp(join2(tmpdir(), "zeta-worker-"));
604
+ worktree = join2(parent, name);
605
+ const add = await git(["worktree", "add", "-b", branch, worktree, "HEAD"], root);
606
+ if (add.code !== 0) {
607
+ return { label, ok: false, error: `git worktree add failed: ${add.out.trim()}` };
608
+ }
609
+ const worktreeCtx = {
610
+ ...deps.ctx,
611
+ cwd: worktree,
612
+ permissions: new Permissions("yolo")
613
+ };
614
+ const tools = workerToolset(worktreeCtx);
615
+ const ac = new AbortController();
616
+ const killer = setTimeout(() => ac.abort(), timeoutMs);
617
+ let summary = "";
618
+ try {
619
+ const r = await generateText2({
620
+ model: deps.model,
621
+ system: WORKER_SYSTEM,
622
+ prompt: task,
623
+ tools,
624
+ stopWhen: stepCountIs2(maxSteps),
625
+ abortSignal: ac.signal
626
+ });
627
+ summary = r.text.trim();
628
+ } catch (e) {
629
+ summary = `(worker aborted or errored before reporting: ${e.message})`;
630
+ } finally {
631
+ clearTimeout(killer);
632
+ }
633
+ await git(["add", "-A"], worktree);
634
+ const statusR = await git(["status", "--porcelain"], worktree);
635
+ const files = statusR.out.split("\n").map((l) => l.trim()).filter(Boolean);
636
+ const diffR = await git(["diff", "--cached", "HEAD"], worktree);
637
+ const diff = diffR.code === 0 ? clampDiff(diffR.out) : "";
638
+ line(c.dim(` \u21B3 ${c.cyan(label)} done \xB7 ${files.length} file(s) changed`));
639
+ return {
640
+ label,
641
+ ok: true,
642
+ summary,
643
+ diff,
644
+ files,
645
+ worktree
646
+ };
647
+ } catch (e) {
648
+ line(c.dim(` \u21B3 ${c.cyan(label)} ${c.red("failed")}`));
649
+ return { label, ok: false, error: e.message, worktree: worktree ?? void 0 };
650
+ } finally {
651
+ if (worktree) {
652
+ await git(["worktree", "remove", "--force", worktree], root);
653
+ await git(["branch", "-D", branch], root);
654
+ const parent = join2(worktree, "..");
655
+ await rm2(parent, { recursive: true, force: true }).catch(() => {
656
+ });
657
+ }
658
+ }
659
+ }
660
+ function buildWorkersTool(deps) {
661
+ const maxSteps = deps.maxSteps ?? 24;
662
+ const maxWorkers = deps.maxWorkers ?? 4;
663
+ const timeoutMs = deps.timeoutMs ?? 3e5;
664
+ return {
665
+ spawn_workers: tool2({
666
+ description: "Run several WRITABLE engineer sub-agents IN PARALLEL, each on its own focused, self-contained slice \u2014 each works in its OWN isolated git worktree so their edits can never stomp the main tree or each other. Each worker can read, write, edit, and run commands inside its sandbox, then returns a summary + the diff of what it changed. Nothing is auto-merged: review each returned diff and apply the good ones yourself. Use for parallel edit-heavy work (scaffold N modules, refactor N files at once). For a single linear edit, just do it directly.",
667
+ inputSchema: z2.object({
668
+ tasks: z2.array(
669
+ z2.object({
670
+ label: z2.string().describe("Short label for this slice, e.g. 'api-route' or 'refactor-auth'."),
671
+ task: z2.string().describe(
672
+ "The complete, self-contained instruction for this worker. It must be doable WITHOUT coordinating with the other workers (they run concurrently in separate worktrees)."
673
+ )
674
+ })
675
+ ).min(1).max(maxWorkers).describe(`1\u2013${maxWorkers} INDEPENDENT, edit-heavy tasks to run concurrently in isolated worktrees.`)
676
+ }),
677
+ execute: async ({ tasks: tasks2 }) => {
678
+ if (deps.ctx.permissions.guardMutation() === "deny") {
679
+ return { ok: false, error: deps.ctx.permissions.planRefusal() };
680
+ }
681
+ const root = await repoRoot(deps.ctx.cwd);
682
+ if (!root) {
683
+ return {
684
+ ok: false,
685
+ error: "spawn_workers needs a git repository (it isolates each worker in a `git worktree`). Run `git init` in the workspace first, or make the edits directly."
686
+ };
687
+ }
688
+ line(c.dim(` \u21C9 spawning ${tasks2.length} writable worker(s) in isolated worktrees`));
689
+ const results = await Promise.all(
690
+ tasks2.map(({ label, task }) => runOneWorker(deps, root, label, task, maxSteps, timeoutMs))
691
+ );
692
+ const changed = results.filter((r) => r.ok && (r.files?.length ?? 0) > 0).length;
693
+ return {
694
+ ok: true,
695
+ count: results.length,
696
+ changed,
697
+ note: "Each result carries a `diff` (unified patch vs HEAD) and `files` (touched paths). The worktrees are already removed \u2014 apply the diffs you want into the main tree yourself (e.g. via write_file/edit_file or by piping the diff through `git apply`), then verify.",
698
+ results
699
+ };
700
+ }
701
+ })
702
+ };
703
+ }
704
+
705
+ // src/ops-subcommand.ts
706
+ import { stdout, cwd } from "process";
707
+ function jsonOut(obj) {
708
+ stdout.write(JSON.stringify(obj) + "\n");
709
+ }
710
+ function base() {
711
+ return webBaseUrl().replace(/\/$/, "");
712
+ }
713
+ async function probe(url, timeoutMs) {
714
+ const controller = new AbortController();
715
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
716
+ const started = Date.now();
717
+ try {
718
+ const resp = await fetch(url, { redirect: "follow", signal: controller.signal });
719
+ return { status: resp.status, latencyMs: Date.now() - started };
720
+ } catch (e) {
721
+ const err = e;
722
+ return {
723
+ latencyMs: Date.now() - started,
724
+ error: err.name === "AbortError" ? `timed out after ${timeoutMs}ms` : err.message
725
+ };
726
+ } finally {
727
+ clearTimeout(timer);
728
+ }
729
+ }
730
+ async function runOpsSubcommand(raw) {
731
+ if (raw[0] !== "ops") return null;
732
+ const rest = raw.slice(1);
733
+ const verb = rest[0];
734
+ const wantsJson = rest.includes("--json");
735
+ const positional = rest.slice(1).filter((a) => !a.startsWith("-"));
736
+ if (verb === "health") {
737
+ const arg = positional[0];
738
+ if (!arg) {
739
+ line(c.red(" usage: wholestack ops health <url>"));
740
+ return 1;
741
+ }
742
+ const origin = (/^https?:\/\//i.test(arg) ? arg : `https://${arg}`).replace(/\/$/, "");
743
+ const root = await probe(origin + "/", 15e3);
744
+ const health = await probe(origin + "/api/health", 15e3);
745
+ const healthy = root.status != null && root.status >= 200 && root.status < 400;
746
+ if (wantsJson) {
747
+ jsonOut({ url: origin, healthy, root, health });
748
+ return healthy ? 0 : 1;
749
+ }
750
+ line("");
751
+ if (root.error) line(" " + c.red(`\u2717 / ${root.error}`));
752
+ else
753
+ line(
754
+ " " + (healthy ? c.green("\u2713 /") : c.yellow("\u2022 /")) + c.dim(` ${root.status} ${root.latencyMs}ms`)
755
+ );
756
+ if (health.error) line(" " + c.dim(`\u2022 /api/health ${health.error}`));
757
+ else line(" " + c.dim(`\u2022 /api/health ${health.status} ${health.latencyMs}ms`));
758
+ return healthy ? 0 : 1;
759
+ }
760
+ if (verb === "status") {
761
+ const deploymentId = positional[0];
762
+ if (!deploymentId) {
763
+ line(c.red(" usage: wholestack ops status <deploymentId> [buildId]"));
764
+ return 1;
765
+ }
766
+ const buildId = positional[1] ?? readProjectLink(cwd())?.buildId;
767
+ if (!buildId) {
768
+ line(c.red(" no build id \u2014 pass one or run inside a linked project."));
769
+ return 1;
770
+ }
771
+ const r = await apiJson(
772
+ `${base()}/api/zeta/build/${encodeURIComponent(buildId)}/deploy?deploymentId=${encodeURIComponent(deploymentId)}`
773
+ );
774
+ if (wantsJson) {
775
+ jsonOut(r.ok ? { ok: true, ...r.data } : { ok: false, error: r.error });
776
+ return r.ok && r.data.status === "ready" ? 0 : 1;
777
+ }
778
+ line("");
779
+ if (!r.ok) {
780
+ line(" " + c.red(`\u2717 ${r.error}`));
781
+ return 1;
782
+ }
783
+ const status = r.data.status ?? "unknown";
784
+ line(" " + (status === "ready" ? c.green(`\u2713 ${status}`) : c.cyan(`\u2022 ${status}`)));
785
+ if (r.data.url) line(" " + c.cyan(r.data.url));
786
+ return status === "ready" ? 0 : status === "error" ? 1 : 0;
787
+ }
788
+ if (verb === "metrics") {
789
+ const r = await readMetrics(base());
790
+ if (wantsJson) {
791
+ jsonOut(r.ok ? { ok: true, metrics: r.metrics } : { ok: false, error: r.error });
792
+ return r.ok ? 0 : 1;
793
+ }
794
+ line("");
795
+ if (!r.ok || !r.metrics) {
796
+ line(" " + c.red(`\u2717 ${r.error ?? "no metrics"}`));
797
+ return 1;
798
+ }
799
+ const m = r.metrics;
800
+ line(
801
+ " connected: " + (m.connected.posthog ? c.green("posthog") : c.dim("posthog")) + " \xB7 " + (m.connected.stripe ? c.green("stripe") : c.dim("stripe"))
802
+ );
803
+ if (m.traffic) line(" " + c.dim(`visitors 7d ${m.traffic.visitors7d} \xB7 signups 7d ${m.traffic.signups7d}`));
804
+ if (m.revenue) line(" " + c.dim(`MRR $${m.revenue.mrr} \xB7 subs ${m.revenue.activeSubscriptions}`));
805
+ return 0;
806
+ }
807
+ if (verb === "errors") {
808
+ const r = await readMetrics(base());
809
+ const errs = r.ok && r.metrics ? r.metrics.errors ?? [] : [];
810
+ const connected = r.ok && r.metrics ? r.metrics.connected.posthog : false;
811
+ if (wantsJson) {
812
+ jsonOut(r.ok ? { ok: true, connected, errors: errs } : { ok: false, error: r.error });
813
+ return r.ok ? 0 : 1;
814
+ }
815
+ line("");
816
+ if (!r.ok || !r.metrics) {
817
+ line(" " + c.red(`\u2717 ${r.error ?? "no metrics"}`));
818
+ return 1;
819
+ }
820
+ if (!connected) {
821
+ line(" " + c.dim("no error provider connected \u2014 connect PostHog (or set SENTRY_* env)."));
822
+ return 0;
823
+ }
824
+ if (errs.length === 0) {
825
+ line(" " + c.green("\u2713 no recent errors"));
826
+ return 0;
827
+ }
828
+ for (const e of errs.slice(0, 10)) {
829
+ line(" " + c.red("\u2022 ") + e.title + c.dim(` \xD7${e.count}`));
830
+ }
831
+ return 0;
832
+ }
833
+ line(c.red(` unknown: wholestack ops ${verb ?? ""}`));
834
+ line(c.dim(" verbs: health <url> \xB7 status <deploymentId> [buildId] \xB7 metrics \xB7 errors (+ --json)"));
835
+ return 1;
836
+ }
837
+
838
+ // src/learned-tools.ts
839
+ import { tool as tool3 } from "ai";
840
+ import { z as z3 } from "zod";
841
+ import { basename } from "path";
842
+ var KIND_VALUES = ["worked", "failed", "quirk", "note"];
843
+ function defaultScope(ctx) {
844
+ return basename(ctx.cwd) || "global";
845
+ }
846
+ function buildLearnedMemoryTools(deps) {
847
+ const { ctx } = deps;
848
+ const { permissions } = ctx;
849
+ return {
850
+ remember: tool3({
851
+ description: "Record ONE durable lesson learned this session so future sessions inherit it (cross-session memory). Use after finishing a non-trivial task to capture: a pattern that WORKED, an approach that FAILED, or a project QUIRK. Keep it to a single atomic fact \u2014 one lesson per call. Auto-loaded into the system prompt at the start of every session.",
852
+ inputSchema: z3.object({
853
+ text: z3.string().min(4).describe("The lesson \u2014 one self-contained, durable fact. Be specific and actionable."),
854
+ kind: z3.enum(KIND_VALUES).default("note").describe("worked = a pattern that worked; failed = a dead end to avoid; quirk = a project gotcha; note = general."),
855
+ tags: z3.array(z3.string()).optional().describe("Optional keywords to make this lesson findable later, e.g. ['prisma','migrations']."),
856
+ scope: z3.string().optional().describe("Project/repo this applies to. Omit to use the current project; use 'global' for everywhere.")
857
+ }),
858
+ execute: async ({ text, kind, tags, scope }) => {
859
+ if (permissions.guardMutation() === "deny") return { ok: false, error: permissions.planRefusal() };
860
+ try {
861
+ const saved = rememberLesson({
862
+ text,
863
+ kind,
864
+ tags,
865
+ scope: scope ?? defaultScope(ctx)
866
+ });
867
+ toolLine("remember", `${saved.kind} \xB7 ${saved.scope}`);
868
+ line(c.dim(` \u21B3 saved ${c.cyan(saved.slug)}`));
869
+ return {
870
+ ok: true,
871
+ slug: saved.slug,
872
+ kind: saved.kind,
873
+ scope: saved.scope,
874
+ tags: saved.tags,
875
+ note: "Lesson stored. It will auto-load into the system prompt next session."
876
+ };
877
+ } catch (e) {
878
+ return { ok: false, error: e.message };
879
+ }
880
+ }
881
+ }),
882
+ recall: tool3({
883
+ description: "Search the lessons you learned in earlier sessions (cross-session memory). Use to check whether you've already solved something here, or to review what worked/failed before. Returns matching lessons newest-first. Lessons from the current project plus any 'global' lessons are always eligible.",
884
+ inputSchema: z3.object({
885
+ query: z3.string().optional().describe("Free-text search over lesson bodies, tags, and kind. Omit to list everything."),
886
+ scope: z3.string().optional().describe("Limit to a project/repo (plus global). Omit to use the current project."),
887
+ limit: z3.number().int().min(1).max(50).default(10).describe("Max lessons to return.")
888
+ }),
889
+ execute: async ({ query, scope, limit }) => {
890
+ try {
891
+ const results = searchLessons({ query, scope: scope ?? defaultScope(ctx), limit });
892
+ toolLine("recall", query ? `"${query}"` : "all lessons");
893
+ line(c.dim(` \u21B3 ${results.length} lesson(s)`));
894
+ return {
895
+ ok: true,
896
+ count: results.length,
897
+ lessons: results.map((l) => ({
898
+ slug: l.slug,
899
+ kind: l.kind,
900
+ scope: l.scope,
901
+ tags: l.tags,
902
+ text: l.text
903
+ }))
904
+ };
905
+ } catch (e) {
906
+ return { ok: false, error: e.message };
907
+ }
908
+ }
909
+ })
910
+ };
911
+ }
912
+
913
+ // src/skills-cli.ts
914
+ import { stdout as stdout2 } from "process";
915
+ async function runSkillsSubcommand(raw) {
916
+ if (raw[0] !== "skills") return null;
917
+ const rest = raw.slice(1);
918
+ const sub = rest[0] && !rest[0].startsWith("-") ? rest[0] : "list";
919
+ const registry = SkillRegistry.load(process.cwd());
920
+ if (sub === "show") {
921
+ const name = rest[1];
922
+ if (!name) {
923
+ line(c.red("usage: wholestack skills show <name>"));
924
+ return 1;
925
+ }
926
+ const skill = registry.get(name);
927
+ if (!skill) {
928
+ line(c.red(`no skill named "${name}"`));
929
+ const suggestions = registry.suggest(name);
930
+ if (suggestions.length) line(c.dim(`did you mean: ${suggestions.join(", ")}`));
931
+ return 1;
932
+ }
933
+ if (rest.includes("--json")) {
934
+ stdout2.write(
935
+ JSON.stringify(
936
+ {
937
+ name: skill.name,
938
+ description: skill.description,
939
+ whenToUse: skill.whenToUse,
940
+ source: skill.source,
941
+ procedure: skill.body
942
+ },
943
+ null,
944
+ 2
945
+ ) + "\n"
946
+ );
947
+ return 0;
948
+ }
949
+ line(c.bold(skill.name) + c.dim(` \xB7 ${skill.source}`));
950
+ line(c.dim(skill.whenToUse));
951
+ line("");
952
+ line(skill.body);
953
+ return 0;
954
+ }
955
+ const skills = registry.list();
956
+ if (rest.includes("--json")) {
957
+ stdout2.write(
958
+ JSON.stringify(
959
+ skills.map((s) => ({
960
+ name: s.name,
961
+ description: s.description,
962
+ whenToUse: s.whenToUse,
963
+ source: s.source
964
+ })),
965
+ null,
966
+ 2
967
+ ) + "\n"
968
+ );
969
+ return 0;
970
+ }
971
+ if (skills.length === 0) {
972
+ line(c.dim("no skills installed."));
973
+ line(c.dim("add *.md files under ~/.wholestack/skills or <cwd>/.wholestack/skills"));
974
+ return 0;
975
+ }
976
+ line(c.bold("skills"));
977
+ const width = Math.max(...skills.map((s) => s.name.length));
978
+ for (const s of skills) {
979
+ line(" " + c.cyan(s.name.padEnd(width)) + " " + s.whenToUse + c.dim(` \xB7 ${s.source}`));
980
+ }
981
+ line("");
982
+ line(c.dim("view one: wholestack skills show <name>"));
983
+ return 0;
984
+ }
985
+
435
986
  // src/images.ts
436
987
  import { readFile as readFile3, stat as stat2 } from "fs/promises";
437
- import { resolve as resolve2, extname, relative as relative2, isAbsolute as isAbsolute2, join as join2 } from "path";
988
+ import { resolve as resolve2, extname, relative as relative2, isAbsolute as isAbsolute2, join as join3 } from "path";
438
989
  import { execFile } from "child_process";
439
- import { tmpdir, platform as platform2 } from "os";
990
+ import { tmpdir as tmpdir2, platform as platform2 } from "os";
440
991
  var EXT_TO_MEDIA = {
441
992
  ".png": "image/png",
442
993
  ".jpg": "image/jpeg",
@@ -455,7 +1006,7 @@ function run(cmd, args) {
455
1006
  });
456
1007
  }
457
1008
  async function grabClipboardImage() {
458
- const out = join2(tmpdir(), `zeta-clip-${process.pid}-${process.hrtime.bigint()}.png`);
1009
+ const out = join3(tmpdir2(), `zeta-clip-${process.pid}-${process.hrtime.bigint()}.png`);
459
1010
  const os = platform2();
460
1011
  if (os === "darwin") {
461
1012
  const script = `set thePath to "${out}"
@@ -489,7 +1040,7 @@ function imageTokens(message) {
489
1040
  for (let m = re.exec(message); m; m = re.exec(message)) out.add(m[1]);
490
1041
  return [...out];
491
1042
  }
492
- async function scanImages(message, cwd2) {
1043
+ async function scanImages(message, cwd3) {
493
1044
  const tokens = imageTokens(message);
494
1045
  const images = [];
495
1046
  const skipped = [];
@@ -518,8 +1069,8 @@ async function scanImages(message, cwd2) {
518
1069
  }
519
1070
  }
520
1071
  for (const tok of tokens) {
521
- const raw = tok.startsWith("~/") ? join2(process.env.HOME ?? "", tok.slice(2)) : tok;
522
- const abs = isAbsolute2(raw) ? raw : resolve2(cwd2, raw);
1072
+ const raw = tok.startsWith("~/") ? join3(process.env.HOME ?? "", tok.slice(2)) : tok;
1073
+ const abs = isAbsolute2(raw) ? raw : resolve2(cwd3, raw);
523
1074
  const mediaType = EXT_TO_MEDIA[extname(abs).toLowerCase()];
524
1075
  if (!mediaType) continue;
525
1076
  let bytes;
@@ -530,7 +1081,7 @@ async function scanImages(message, cwd2) {
530
1081
  } catch {
531
1082
  continue;
532
1083
  }
533
- const label = isAbsolute2(raw) ? raw : relative2(cwd2, abs);
1084
+ const label = isAbsolute2(raw) ? raw : relative2(cwd3, abs);
534
1085
  if (bytes > PER_IMAGE_BYTES || total + bytes > TOTAL_BYTES) {
535
1086
  skipped.push(`${label} (${Math.round(bytes / 1024)}KB \u2014 over the size cap)`);
536
1087
  continue;
@@ -664,21 +1215,21 @@ ${text}`;
664
1215
  ${body}
665
1216
  `;
666
1217
  }
667
- function writeTranscript(messages, meta, file, cwd2) {
668
- const abs = resolve3(cwd2, file);
1218
+ function writeTranscript(messages, meta, file, cwd3) {
1219
+ const abs = resolve3(cwd3, file);
669
1220
  writeFileSync(abs, renderTranscript(messages, meta), "utf8");
670
1221
  return abs;
671
1222
  }
672
1223
 
673
1224
  // src/cli.ts
674
1225
  import { existsSync as existsSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
675
- import { join as join5 } from "path";
1226
+ import { join as join6 } from "path";
676
1227
 
677
1228
  // src/update-check.ts
678
1229
  import { existsSync, mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "fs";
679
- import { join as join3 } from "path";
1230
+ import { join as join4 } from "path";
680
1231
  var PKG = "wholestack";
681
- var CACHE = join3(stateDir(), "update-check.json");
1232
+ var CACHE = join4(stateDir(), "update-check.json");
682
1233
  var DAY_MS = 24 * 60 * 60 * 1e3;
683
1234
  function readCache() {
684
1235
  try {
@@ -732,8 +1283,8 @@ async function checkForUpdate(current) {
732
1283
 
733
1284
  // src/access.ts
734
1285
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync3 } from "fs";
735
- import { join as join4 } from "path";
736
- var CACHE2 = join4(stateDir(), "access.json");
1286
+ import { join as join5 } from "path";
1287
+ var CACHE2 = join5(stateDir(), "access.json");
737
1288
  var TTL_MS = 60 * 60 * 1e3;
738
1289
  function readCache2() {
739
1290
  try {
@@ -756,10 +1307,18 @@ function clearAccessCache() {
756
1307
  }
757
1308
  }
758
1309
  async function verifyAccess(webUrl, token, force = false) {
759
- if (!token) return { active: false, tier: null, degraded: false };
1310
+ if (!token) {
1311
+ return { active: false, paid: false, tier: null, freeDeliveries: null, degraded: false };
1312
+ }
760
1313
  const cached = force ? null : readCache2();
761
1314
  if (cached && Date.now() - cached.checkedAt < TTL_MS) {
762
- return { active: cached.active, tier: cached.tier, degraded: false };
1315
+ return {
1316
+ active: cached.active,
1317
+ paid: cached.paid,
1318
+ tier: cached.tier,
1319
+ freeDeliveries: cached.freeDeliveries,
1320
+ degraded: false
1321
+ };
763
1322
  }
764
1323
  try {
765
1324
  const ctrl = new AbortController();
@@ -770,18 +1329,44 @@ async function verifyAccess(webUrl, token, force = false) {
770
1329
  });
771
1330
  clearTimeout(t);
772
1331
  if (res.status >= 500) {
773
- return { active: true, tier: cached?.tier ?? null, degraded: true };
1332
+ return {
1333
+ active: true,
1334
+ paid: cached?.paid ?? false,
1335
+ tier: cached?.tier ?? null,
1336
+ freeDeliveries: cached?.freeDeliveries ?? null,
1337
+ degraded: true
1338
+ };
774
1339
  }
775
1340
  if (!res.ok) {
776
- writeCache2({ checkedAt: Date.now(), active: false, tier: null });
777
- return { active: false, tier: null, degraded: false };
1341
+ writeCache2({
1342
+ checkedAt: Date.now(),
1343
+ active: false,
1344
+ paid: false,
1345
+ tier: null,
1346
+ freeDeliveries: null
1347
+ });
1348
+ return { active: false, paid: false, tier: null, freeDeliveries: null, degraded: false };
778
1349
  }
779
1350
  const json = await res.json();
780
1351
  const active = Boolean(json.active);
781
- writeCache2({ checkedAt: Date.now(), active, tier: json.tier ?? null });
782
- return { active, tier: json.tier ?? null, degraded: false };
1352
+ const paid = Boolean(json.paid);
1353
+ const freeDeliveries = json.freeDeliveries ?? null;
1354
+ writeCache2({
1355
+ checkedAt: Date.now(),
1356
+ active,
1357
+ paid,
1358
+ tier: json.tier ?? null,
1359
+ freeDeliveries
1360
+ });
1361
+ return { active, paid, tier: json.tier ?? null, freeDeliveries, degraded: false };
783
1362
  } catch {
784
- return { active: true, tier: cached?.tier ?? null, degraded: true };
1363
+ return {
1364
+ active: true,
1365
+ paid: cached?.paid ?? false,
1366
+ tier: cached?.tier ?? null,
1367
+ freeDeliveries: cached?.freeDeliveries ?? null,
1368
+ degraded: true
1369
+ };
785
1370
  }
786
1371
  }
787
1372
  function showPaywall(loggedIn, webUrl) {
@@ -789,36 +1374,36 @@ function showPaywall(loggedIn, webUrl) {
789
1374
  line();
790
1375
  line(" " + c.cyan("\u256D" + "\u2500".repeat(w) + "\u256E"));
791
1376
  const row = (s) => line(" " + c.cyan("\u2502") + " " + s.padEnd(w - 2) + " " + c.cyan("\u2502"));
792
- row(c.bold("zeta needs a subscription"));
1377
+ row(c.bold("wholestack needs you signed in"));
793
1378
  row("");
794
1379
  if (!loggedIn) {
795
1380
  row(c.dim("You're not signed in."));
796
- row("Run " + c.cyan("zeta login") + c.dim(" then subscribe."));
1381
+ row("Run " + c.cyan("wholestack login") + c.dim(" to start."));
797
1382
  } else {
798
- row(c.dim("No active paid plan on this account."));
799
- row("Subscribe to use zeta in the terminal.");
1383
+ row(c.dim("Your token was rejected or expired."));
1384
+ row("Run " + c.cyan("wholestack login") + c.dim(" again."));
800
1385
  }
801
1386
  row("");
802
- row(c.dim("Plans: ") + c.cyan(`${webUrl.replace(/\/$/, "")}/pricing`));
1387
+ row(c.dim("Free: 3 delivered apps/month \xB7 then ") + c.cyan(`${webUrl.replace(/\/$/, "")}/pricing`));
803
1388
  line(" " + c.cyan("\u2570" + "\u2500".repeat(w) + "\u256F"));
804
1389
  line();
805
1390
  }
806
1391
 
807
1392
  // src/cli.ts
808
1393
  import { readFileSync as readFileSync3 } from "fs";
809
- import { basename, dirname as dirname2 } from "path";
1394
+ import { basename as basename2, dirname as dirname2 } from "path";
810
1395
  import { fileURLToPath } from "url";
811
1396
  var VERSION = (() => {
812
1397
  try {
813
1398
  const here = dirname2(fileURLToPath(import.meta.url));
814
- return JSON.parse(readFileSync3(join5(here, "..", "package.json"), "utf8")).version;
1399
+ return JSON.parse(readFileSync3(join6(here, "..", "package.json"), "utf8")).version;
815
1400
  } catch {
816
1401
  return "0.0.0";
817
1402
  }
818
1403
  })();
819
1404
  function gitBranch(dir) {
820
1405
  try {
821
- const head = readFileSync3(join5(dir, ".git", "HEAD"), "utf8").trim();
1406
+ const head = readFileSync3(join6(dir, ".git", "HEAD"), "utf8").trim();
822
1407
  const m = head.match(/ref:\s*refs\/heads\/(.+)$/);
823
1408
  return m ? m[1] : head.slice(0, 7);
824
1409
  } catch {
@@ -885,13 +1470,39 @@ ${c.bold("Projects")} ${c.dim("(one spine across terminal, dashboard and the web
885
1470
  wholestack push [dir] sync local edits back to the web IDE
886
1471
  wholestack open [buildId] open a generated build in the web IDE
887
1472
 
1473
+ ${c.bold("Ship")} ${c.dim("(go live \u2014 same credits + entitlements as the web app)")}
1474
+ wholestack deploy [buildId] live preview deploy (polls to ready)
1475
+ wholestack promote [buildId] [--domain d] durable production deploy (real DB)
1476
+ wholestack rollback [buildId] --deployment <id> restore the previous production deploy
1477
+ ${c.dim("buildId defaults to the linked build in this dir (after a delivery or `open`)")}
1478
+
1479
+ ${c.bold("Launch")} ${c.dim("(the studio's Launch HQ \u2014 go-to-market, in the terminal)")}
1480
+ wholestack launch [buildId] [--prompt "\u2026"] draw the AI go-to-market plan
1481
+ wholestack launch-kit [buildId] show the build's startup kit (brand/pricing/copy/\u2026)
1482
+ wholestack legal <terms|privacy|cookie|refund> generate a SaaS legal doc (Markdown \u2192 stdout)
1483
+ wholestack metrics your REAL traffic + revenue (PostHog + Stripe)
1484
+ wholestack waitlist [buildId] live waitlist signup count
1485
+ wholestack businesses every business workspace you own
1486
+ wholestack business [buildId] one workspace + its readiness scorecard
1487
+ ${c.dim("add --json for machine-readable output")}
1488
+
1489
+ ${c.bold("Prove gate (web2)")} ${c.dim("(deterministic \xB7 fail-closed \xB7 blocking exit code)")}
1490
+ wholestack prove <dir> [--json] gate a checked-out repo (local lockstep mirror)
1491
+ wholestack prove --remote --github o/r [--ref b] hosted, HMAC-signed verdict via /api/v1/prove
1492
+ ${c.dim("checks: secrets/sinks/unauth-mutations + authorization-completeness (no orphan routes)")}
1493
+
888
1494
  ${c.bold("Web3 security")} ${c.dim("(forge \xB7 slither \xB7 firewall \xB7 halmos \xB7 signed cert)")}
889
- wholestack audit <dir> <Contract> full auto-detect security sweep
1495
+ wholestack audit <dir> <Contract> full auto-detect security sweep (local)
1496
+ wholestack audit repo [--github o/r] [--pick] remote repo audit + live SSE progress
890
1497
  wholestack prove <dir> <Contract> --property <k> prove one invariant
891
1498
  wholestack verify <dir> <Contract> --cert c.json passthrough + signed certificate
892
1499
  ${c.dim("kinds: reentrancy-safety, access-control, conservation, no-value-extraction, \u2026")}
893
1500
  wholestack --nzt48 "audit ./contracts" NZT-48 web3 special-ops persona
894
1501
 
1502
+ ${c.bold("GitHub")} ${c.dim("(private repos via App installation)")}
1503
+ wholestack github installations list linked GitHub App installs
1504
+ wholestack github repos --installation <id> list repos for an installation
1505
+
895
1506
  ${c.bold("Session")}
896
1507
  -m, --model <key> zeta-g1-lite | zeta-g1 | zeta-g1-max | vision
897
1508
  (lite = light \xB7 g1 = default \xB7 max = most capable \xB7 vision = sees images)
@@ -929,10 +1540,218 @@ ${c.bold("In a session")} ${c.dim("(type /help for the full list)")}
929
1540
  ${c.dim("@shot.png \xB7 @clip attach an image / paste from clipboard (auto-switches to Vision)")}
930
1541
  ${c.dim("@https://\u2026 fetch a web page and attach its text to the turn")}
931
1542
  ${c.dim("spawn_agents tool fan a task out to parallel read-only sub-agents")}`;
1543
+ function flagValue2(rest, flag) {
1544
+ const i = rest.indexOf(flag);
1545
+ return i >= 0 ? rest[i + 1] : void 0;
1546
+ }
1547
+ async function runGithubSubcommand(raw) {
1548
+ if (raw[0] !== "github") return null;
1549
+ const sub = raw[1];
1550
+ const base2 = webBaseUrl();
1551
+ if (sub === "installations") {
1552
+ const r = await fetchGithubInstallations(base2);
1553
+ if (!r.ok) {
1554
+ line(c.red(` ${r.error}`));
1555
+ return 1;
1556
+ }
1557
+ if (!r.installations.length) {
1558
+ line(c.dim(" no installations \u2014 connect GitHub in the web dashboard."));
1559
+ return 0;
1560
+ }
1561
+ line();
1562
+ for (const row of r.installations) {
1563
+ const suspended = row.suspendedAt ? c.yellow(" \xB7 suspended") : "";
1564
+ line(` ${c.cyan(String(row.installationId))} ${row.accountLogin}${suspended}`);
1565
+ }
1566
+ line();
1567
+ return 0;
1568
+ }
1569
+ if (sub === "repos") {
1570
+ const rawId = flagValue2(raw.slice(2), "--installation");
1571
+ const installationId = rawId ? Number(rawId) : NaN;
1572
+ if (!rawId || !Number.isFinite(installationId)) {
1573
+ line(c.red(" usage: wholestack github repos --installation <id>"));
1574
+ return 1;
1575
+ }
1576
+ const r = await fetchGithubRepos(base2, installationId);
1577
+ if (!r.ok) {
1578
+ line(c.red(` ${r.error}`));
1579
+ return 1;
1580
+ }
1581
+ line();
1582
+ for (const repo of r.repos) {
1583
+ line(
1584
+ ` ${repo.fullName}${repo.private ? c.yellow(" \xB7 private") : ""} ${c.dim(repo.defaultBranch ?? "")}`
1585
+ );
1586
+ }
1587
+ line();
1588
+ return 0;
1589
+ }
1590
+ line(c.red(" usage: wholestack github installations | wholestack github repos --installation <id>"));
1591
+ return 1;
1592
+ }
1593
+ async function runAuditRepoSubcommand(rest) {
1594
+ const base2 = webBaseUrl();
1595
+ const body = {};
1596
+ if (rest.includes("--pick")) {
1597
+ const picked = await pickGithubRepoInteractive(base2);
1598
+ if (!picked.ok) {
1599
+ line(c.red(` ${picked.error}`));
1600
+ return 1;
1601
+ }
1602
+ body.githubRepo = picked.githubRepo;
1603
+ body.installationId = picked.installationId;
1604
+ }
1605
+ const github = flagValue2(rest, "--github");
1606
+ if (github) body.githubRepo = github;
1607
+ const installationRaw = flagValue2(rest, "--installation");
1608
+ if (installationRaw) {
1609
+ const n = Number(installationRaw);
1610
+ if (!Number.isFinite(n)) {
1611
+ line(c.red(" --installation must be a number"));
1612
+ return 1;
1613
+ }
1614
+ body.installationId = n;
1615
+ }
1616
+ const gitUrl = flagValue2(rest, "--git-url");
1617
+ if (gitUrl) body.gitUrl = gitUrl;
1618
+ const sourcePath = flagValue2(rest, "--source");
1619
+ if (sourcePath) {
1620
+ const src = readSourceFile(sourcePath);
1621
+ if (!src.ok) {
1622
+ line(c.red(` ${src.error}`));
1623
+ return 1;
1624
+ }
1625
+ body.source = src.source;
1626
+ }
1627
+ const maxRaw = flagValue2(rest, "--max-contracts");
1628
+ if (maxRaw) {
1629
+ const n = Number(maxRaw);
1630
+ if (!Number.isFinite(n)) {
1631
+ line(c.red(" --max-contracts must be a number"));
1632
+ return 1;
1633
+ }
1634
+ body.maxContracts = n;
1635
+ }
1636
+ if (!body.source && !body.gitUrl && !body.githubRepo) {
1637
+ line(c.red(" provide --github owner/repo, --git-url, --source <file>, or --pick"));
1638
+ return 1;
1639
+ }
1640
+ line();
1641
+ line(c.cyan(" \u259F ShipGate repo audit") + c.dim(" \xB7 streaming progress"));
1642
+ line();
1643
+ const r = await runRepoAuditStream(base2, body);
1644
+ line();
1645
+ if (!r.ok) {
1646
+ line(c.red(` \u2717 ${r.error}`));
1647
+ if (r.status === 402 && r.upgradeUrl) {
1648
+ line(c.dim(` upgrade: ${base2}${r.upgradeUrl}`));
1649
+ }
1650
+ return 1;
1651
+ }
1652
+ const verdict = String(r.result.verdict ?? r.result.shipVerdict ?? "unknown");
1653
+ line(verdict === "SHIP" || verdict === "ship" ? c.green(" \u2713 SHIP") : c.yellow(` verdict: ${verdict}`));
1654
+ if (r.result.certificateUrl) line(c.dim(` certificate: ${r.result.certificateUrl}`));
1655
+ if (r.result.bundleId) line(c.dim(` bundle: ${base2}/api/audit/bundle/${r.result.bundleId}`));
1656
+ return 0;
1657
+ }
1658
+ async function runProveSubcommand(raw) {
1659
+ if (raw[0] !== "prove") return null;
1660
+ const rest = raw.slice(1);
1661
+ if (rest.includes("--property")) return null;
1662
+ const wantsJson = rest.includes("--json");
1663
+ if (rest.includes("--remote")) {
1664
+ const base2 = webBaseUrl().replace(/\/$/, "");
1665
+ const key = process.env.ZETA_API_KEY?.trim();
1666
+ if (!key) {
1667
+ line(c.red(" not signed in \u2014 run `wholestack login` first (--remote needs a token)."));
1668
+ return 1;
1669
+ }
1670
+ const body = {};
1671
+ const github = flagValue2(rest, "--github");
1672
+ if (github) body.githubRepo = github;
1673
+ const gitUrl = flagValue2(rest, "--git-url");
1674
+ if (gitUrl) body.gitUrl = gitUrl;
1675
+ const installationRaw = flagValue2(rest, "--installation");
1676
+ if (installationRaw) {
1677
+ const n = Number(installationRaw);
1678
+ if (!Number.isFinite(n)) {
1679
+ line(c.red(" --installation must be a number"));
1680
+ return 1;
1681
+ }
1682
+ body.installationId = n;
1683
+ }
1684
+ const ref = flagValue2(rest, "--ref");
1685
+ if (ref) body.ref = ref;
1686
+ if (!body.githubRepo && !body.gitUrl) {
1687
+ line(c.red(" --remote needs --github owner/repo or --git-url <url>"));
1688
+ return 1;
1689
+ }
1690
+ let resp;
1691
+ try {
1692
+ resp = await fetch(`${base2}/api/v1/prove`, {
1693
+ method: "POST",
1694
+ headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
1695
+ body: JSON.stringify(body)
1696
+ });
1697
+ } catch (e) {
1698
+ line(c.red(` cannot reach the platform: ${e.message}`));
1699
+ return 1;
1700
+ }
1701
+ const data = await resp.json().catch(() => null);
1702
+ if (wantsJson) {
1703
+ stdout3.write(JSON.stringify(data) + "\n");
1704
+ return data?.verdict === "SHIP" ? 0 : 1;
1705
+ }
1706
+ if (resp.status === 401 || resp.status === 402 || resp.status === 503 || !data) {
1707
+ line(c.red(` \u2717 ${data?.error ?? `platform returned ${resp.status}`}`));
1708
+ if (data?.upgradeUrl) line(c.dim(` upgrade: ${base2}${data.upgradeUrl}`));
1709
+ return 1;
1710
+ }
1711
+ printProveVerdict(data.verdict ?? "NO_SHIP", data.score ?? 0, data.checks ?? [], data.blockingReasons ?? []);
1712
+ return data.verdict === "SHIP" ? 0 : 1;
1713
+ }
1714
+ const path = rest.find((a) => !a.startsWith("-")) ?? cwd2();
1715
+ if (!existsSync2(path)) {
1716
+ line(c.red(` path not found: ${path}`));
1717
+ return 1;
1718
+ }
1719
+ const result = proveLocalRepo(path);
1720
+ if (wantsJson) {
1721
+ stdout3.write(JSON.stringify(result) + "\n");
1722
+ return result.verdict === "SHIP" ? 0 : 1;
1723
+ }
1724
+ line();
1725
+ line(c.cyan(" \u259F ISL prove-gate") + c.dim(" \xB7 deterministic \xB7 fail-closed \xB7 foreign default-deny"));
1726
+ line();
1727
+ printProveVerdict(result.verdict, result.score, result.checks, result.blockingReasons);
1728
+ return result.verdict === "SHIP" ? 0 : 1;
1729
+ }
1730
+ function printProveVerdict(verdict, score, checks, blockingReasons) {
1731
+ for (const ch of checks) {
1732
+ const icon = ch.status === "pass" ? c.green("\u2713") : ch.status === "fail" ? c.red("\u2717") : c.dim("\u2205");
1733
+ const label = ch.status === "not-run" ? c.dim(ch.title + " (not run)") : ch.title;
1734
+ line(` ${icon} ${label}`);
1735
+ line(c.dim(` ${ch.detail}`));
1736
+ }
1737
+ line();
1738
+ if (blockingReasons.length) {
1739
+ line(c.red(` ${blockingReasons.length} blocking reason(s):`));
1740
+ for (const r of blockingReasons.slice(0, 25)) line(c.dim(` - ${r}`));
1741
+ if (blockingReasons.length > 25) line(c.dim(` \u2026+${blockingReasons.length - 25} more`));
1742
+ line();
1743
+ }
1744
+ line(
1745
+ verdict === "SHIP" ? c.green(` \u2713 SHIP`) + c.dim(` \xB7 score ${score}/100`) : c.red(` \u2717 NO_SHIP`) + c.dim(` \xB7 score ${score}/100`)
1746
+ );
1747
+ }
932
1748
  async function runSecuritySubcommand(raw) {
933
1749
  const verb = raw[0];
934
1750
  if (verb !== "audit" && verb !== "prove" && verb !== "verify") return null;
935
1751
  const rest = raw.slice(1);
1752
+ if (verb === "audit" && rest[0] === "repo") {
1753
+ return runAuditRepoSubcommand(rest.slice(1));
1754
+ }
936
1755
  const proverArgs = verb === "verify" && rest[0]?.endsWith(".json") ? ["verify", ...rest] : [...rest];
937
1756
  if (verb === "prove" && !proverArgs.includes("--property")) {
938
1757
  line(c.red(" prove needs --property <kind>"));
@@ -942,7 +1761,7 @@ async function runSecuritySubcommand(raw) {
942
1761
  line();
943
1762
  line(c.cyan(` \u259F ShipGate ${verb}`) + c.dim(" \xB7 forge \xB7 slither \xB7 firewall \xB7 halmos"));
944
1763
  line();
945
- const r = await runProver(proverArgs, { cwd: cwd(), inherit: true });
1764
+ const r = await runProver(proverArgs, { cwd: cwd2(), inherit: true });
946
1765
  if (r.missing) {
947
1766
  line(c.red(` ${r.out}`));
948
1767
  return 1;
@@ -954,7 +1773,7 @@ async function runSecuritySubcommand(raw) {
954
1773
  function maybeYoloNotice() {
955
1774
  try {
956
1775
  const dir = stateDir();
957
- const marker = join5(dir, "yolo-notice-seen");
1776
+ const marker = join6(dir, "yolo-notice-seen");
958
1777
  if (existsSync2(marker)) return;
959
1778
  line(
960
1779
  " " + c.yellow("\u26A1 yolo mode") + c.dim(" \u2014 actions run without confirmation. ") + c.cyan("/mode default") + c.dim(" to require approvals \xB7 /undo reverts.") + "\n"
@@ -964,9 +1783,6 @@ function maybeYoloNotice() {
964
1783
  } catch {
965
1784
  }
966
1785
  }
967
- function webBaseUrl() {
968
- return process.env.ZETA_WEB_URL?.trim() || process.env.ZETA_API_URL?.trim() || "https://wholestack.ai";
969
- }
970
1786
  async function runLogin(raw) {
971
1787
  if (raw[0] !== "login" && raw[0] !== "logout") return null;
972
1788
  const rest = raw.slice(1);
@@ -1018,7 +1834,7 @@ async function runLogin(raw) {
1018
1834
  line(c.red(" usage: wholestack login --member <key> | --brain <key> | --build <key>"));
1019
1835
  return 1;
1020
1836
  }
1021
- const rl = createInterface({ input: stdin, output: stdout });
1837
+ const rl = createInterface({ input: stdin, output: stdout3 });
1022
1838
  if (!name) {
1023
1839
  const which = (await rl.question(c.cyan(" key for [member/brain/engine]: "))).trim().toLowerCase();
1024
1840
  name = which.startsWith("m") ? "ZETA_API_KEY" : "CEREBRAS_API_KEY";
@@ -1040,9 +1856,9 @@ async function runLogin(raw) {
1040
1856
  async function runProjectSubcommand(raw) {
1041
1857
  const verb = raw[0];
1042
1858
  if (verb !== "projects" && verb !== "pull" && verb !== "push" && verb !== "open") return null;
1043
- const base = webBaseUrl();
1859
+ const base2 = webBaseUrl();
1044
1860
  if (verb === "projects") {
1045
- const r2 = await listProjects(base);
1861
+ const r2 = await listProjects(base2);
1046
1862
  if (!r2.ok) {
1047
1863
  line(c.red(` ${r2.error}`));
1048
1864
  return 1;
@@ -1058,7 +1874,7 @@ async function runProjectSubcommand(raw) {
1058
1874
  if (p.description) line(" " + c.dim(` ${p.description}`));
1059
1875
  }
1060
1876
  line();
1061
- line(c.dim(" pull one with ") + c.cyan("wholestack pull <id>") + c.dim(" \xB7 open in the IDE at ") + c.cyan(`${base}/ide/studio?projectId=<id>`));
1877
+ line(c.dim(" pull one with ") + c.cyan("wholestack pull <id>") + c.dim(" \xB7 open in the IDE at ") + c.cyan(`${base2}/ide/studio?projectId=<id>`));
1062
1878
  return 0;
1063
1879
  }
1064
1880
  if (verb === "pull") {
@@ -1067,7 +1883,7 @@ async function runProjectSubcommand(raw) {
1067
1883
  line(c.red(" usage: wholestack pull <projectId> [dir]"));
1068
1884
  return 1;
1069
1885
  }
1070
- const r2 = await pullWorkingTree(base, projectId);
1886
+ const r2 = await pullWorkingTree(base2, projectId);
1071
1887
  if (!r2.ok) {
1072
1888
  line(c.red(` ${r2.error}`));
1073
1889
  return 1;
@@ -1093,14 +1909,14 @@ async function runProjectSubcommand(raw) {
1093
1909
  line(c.red(" nothing to push (no readable text files found)."));
1094
1910
  return 1;
1095
1911
  }
1096
- const r2 = await pushWorkingTree(base, projectId, files);
1912
+ const r2 = await pushWorkingTree(base2, projectId, files);
1097
1913
  if (!r2.ok) {
1098
1914
  line(c.red(` ${r2.error}`));
1099
1915
  return 1;
1100
1916
  }
1101
1917
  writeProjectLink(dir, { projectId });
1102
1918
  line(c.green(` \u2713 pushed ${files.length} files`) + c.dim(skipped ? ` (${skipped} skipped: binary/oversize)` : ""));
1103
- line(c.dim(" open it: ") + c.cyan(`${base}/ide/studio?projectId=${projectId}`));
1919
+ line(c.dim(" open it: ") + c.cyan(`${base2}/ide/studio?projectId=${projectId}`));
1104
1920
  return 0;
1105
1921
  }
1106
1922
  const buildId = raw[1] ?? readProjectLink(".")?.buildId;
@@ -1108,14 +1924,189 @@ async function runProjectSubcommand(raw) {
1108
1924
  line(c.red(" usage: wholestack open <buildId> (or run it inside a delivered app dir)"));
1109
1925
  return 1;
1110
1926
  }
1111
- const r = await openBuildInEditor(base, buildId);
1927
+ const r = await openBuildInEditor(base2, buildId);
1112
1928
  if (!r.ok) {
1113
1929
  line(c.red(` ${r.error}`));
1114
1930
  return 1;
1115
1931
  }
1116
1932
  if (r.projectId) writeProjectLink(".", { buildId, projectId: r.projectId });
1117
1933
  line(c.green(" \u2713 project ready in the web IDE"));
1118
- line(c.dim(" ") + c.cyan(`${base}${r.editorUrl ?? `/ide/studio?projectId=${r.projectId}`}`));
1934
+ line(c.dim(" ") + c.cyan(`${base2}${r.editorUrl ?? `/ide/studio?projectId=${r.projectId}`}`));
1935
+ return 0;
1936
+ }
1937
+ async function runShipSubcommand(raw) {
1938
+ const verb = raw[0];
1939
+ if (verb !== "deploy" && verb !== "promote" && verb !== "rollback") return null;
1940
+ const base2 = webBaseUrl();
1941
+ const rest = raw.slice(1);
1942
+ const buildId = (rest.find((a) => !a.startsWith("-")) ?? readProjectLink(".")?.buildId)?.trim();
1943
+ if (!buildId) {
1944
+ line(c.red(` usage: wholestack ${verb} <buildId> (or run it inside a delivered app dir)`));
1945
+ return 1;
1946
+ }
1947
+ if (verb === "deploy") {
1948
+ line();
1949
+ line(c.cyan(" \u259F deploy") + c.dim(" \xB7 live preview \xB7 charges one build credit (paid once per build)"));
1950
+ const r2 = await deployBuild(base2, buildId, {
1951
+ poll: true,
1952
+ onPhase: (s) => line(c.dim(` \xB7 ${s}`))
1953
+ });
1954
+ return printDeploy(r2, base2, "deployed");
1955
+ }
1956
+ if (verb === "promote") {
1957
+ const domain = flagValue2(rest, "--domain");
1958
+ line();
1959
+ line(c.cyan(" \u259F promote") + c.dim(" \xB7 durable production \xB7 real database, dev-auth OFF"));
1960
+ const r2 = await promoteBuild(base2, buildId, domain ? { domain } : {});
1961
+ return printDeploy(r2, base2, "promoted to production");
1962
+ }
1963
+ const deploymentId = flagValue2(rest, "--deployment") ?? flagValue2(rest, "--deployment-id");
1964
+ if (!deploymentId) {
1965
+ line(c.red(" usage: wholestack rollback [buildId] --deployment <currentDeploymentId>"));
1966
+ return 1;
1967
+ }
1968
+ line();
1969
+ line(c.cyan(" \u259F rollback") + c.dim(" \xB7 restore the previous production deployment"));
1970
+ const r = await rollbackDeployment(base2, buildId, deploymentId);
1971
+ return printDeploy(r, base2, "rolled back");
1972
+ }
1973
+ function printDeploy(r, base2, doneVerb) {
1974
+ line();
1975
+ if (!r.ok) {
1976
+ line(c.red(` \u2717 ${r.error}`));
1977
+ if (r.hint) line(c.dim(` ${r.hint}`));
1978
+ if (r.upgradeUrl) line(c.dim(` upgrade: ${base2.replace(/\/$/, "")}${r.upgradeUrl}`));
1979
+ if (r.connectUrl) line(c.dim(` connect: ${base2.replace(/\/$/, "")}${r.connectUrl}`));
1980
+ return 1;
1981
+ }
1982
+ line(c.green(` \u2713 ${doneVerb}`) + (r.status ? c.dim(` \xB7 ${r.status}`) : ""));
1983
+ if (r.url) line(c.dim(" ") + c.cyan(r.url.startsWith("http") ? r.url : `https://${r.url}`));
1984
+ if (r.deploymentId) line(c.dim(` deployment ${r.deploymentId}`));
1985
+ if (r.dbNote) line(c.dim(` ${r.dbNote}`));
1986
+ else if (r.dbProvisioned) line(c.dim(" database provisioned + schema applied"));
1987
+ return 0;
1988
+ }
1989
+ async function runLaunchSubcommand(raw) {
1990
+ const verb = raw[0];
1991
+ if (verb !== "launch" && verb !== "launch-kit") return null;
1992
+ const base2 = webBaseUrl();
1993
+ const rest = raw.slice(1);
1994
+ const wantsJson = rest.includes("--json");
1995
+ const argBuildId = rest.find((a) => !a.startsWith("-"));
1996
+ const buildId = (argBuildId ?? readProjectLink(".")?.buildId)?.trim();
1997
+ if (verb === "launch") {
1998
+ const prompt = flagValue2(rest, "--prompt");
1999
+ line();
2000
+ line(c.cyan(" \u259F launch canvas") + c.dim(" \xB7 drawing your go-to-market plan\u2026"));
2001
+ const r2 = await drawCanvas(base2, { buildId, prompt });
2002
+ if (!r2.ok || !r2.plan) {
2003
+ line(c.red(` \u2717 ${r2.error ?? "no plan returned"}`));
2004
+ return 1;
2005
+ }
2006
+ if (wantsJson) {
2007
+ stdout3.write(JSON.stringify(r2.plan, null, 2) + "\n");
2008
+ return 0;
2009
+ }
2010
+ for (const ln of renderGtmPlanLines(r2.plan)) line(ln);
2011
+ return 0;
2012
+ }
2013
+ if (!buildId) {
2014
+ line(c.red(" usage: wholestack launch-kit <buildId> (or run it inside a delivered app dir)"));
2015
+ return 1;
2016
+ }
2017
+ const r = await getLaunchKit(base2, buildId);
2018
+ if (!r.ok || !r.kit) {
2019
+ line(c.red(` \u2717 ${r.error ?? "no launch kit"}`));
2020
+ return 1;
2021
+ }
2022
+ if (wantsJson) {
2023
+ stdout3.write(JSON.stringify(r.kit, null, 2) + "\n");
2024
+ return 0;
2025
+ }
2026
+ for (const ln of renderLaunchKitLines(r.kit)) line(ln);
2027
+ return 0;
2028
+ }
2029
+ async function runHqSubcommand(raw) {
2030
+ const verb = raw[0];
2031
+ if (!["legal", "metrics", "waitlist", "businesses", "business"].includes(verb)) return null;
2032
+ const base2 = webBaseUrl();
2033
+ const rest = raw.slice(1);
2034
+ const wantsJson = rest.includes("--json");
2035
+ const firstArg = rest.find((a) => !a.startsWith("-"));
2036
+ if (verb === "legal") {
2037
+ const kinds = ["terms", "privacy", "cookie", "refund"];
2038
+ const kind = firstArg;
2039
+ if (!kind || !kinds.includes(kind)) {
2040
+ line(c.red(" usage: wholestack legal <terms|privacy|cookie|refund> [--name <business>]"));
2041
+ return 1;
2042
+ }
2043
+ const r2 = await generateLegal(base2, { kind, businessName: flagValue2(rest, "--name") });
2044
+ if (!r2.ok || !r2.markdown) {
2045
+ line(c.red(` \u2717 ${r2.error ?? "no document produced"}`));
2046
+ return 1;
2047
+ }
2048
+ stdout3.write(r2.markdown.trimEnd() + "\n");
2049
+ return 0;
2050
+ }
2051
+ if (verb === "metrics") {
2052
+ const r2 = await readMetrics(base2);
2053
+ if (!r2.ok || !r2.metrics) {
2054
+ line(c.red(` \u2717 ${r2.error ?? "could not read metrics"}`));
2055
+ return 1;
2056
+ }
2057
+ if (wantsJson) {
2058
+ stdout3.write(JSON.stringify(r2.metrics, null, 2) + "\n");
2059
+ return 0;
2060
+ }
2061
+ for (const ln of renderMetricsLines(r2.metrics)) line(ln);
2062
+ return 0;
2063
+ }
2064
+ if (verb === "waitlist") {
2065
+ const buildId2 = (firstArg ?? readProjectLink(".")?.buildId)?.trim();
2066
+ if (!buildId2) {
2067
+ line(c.red(" usage: wholestack waitlist <buildId> (or run it inside a delivered app dir)"));
2068
+ return 1;
2069
+ }
2070
+ const r2 = await waitlistCount(base2, buildId2);
2071
+ if (!r2.ok) {
2072
+ line(c.red(` \u2717 ${r2.error}`));
2073
+ return 1;
2074
+ }
2075
+ line();
2076
+ line(" " + c.bold(String(r2.count ?? 0)) + c.dim(" waitlist signup(s)"));
2077
+ return 0;
2078
+ }
2079
+ if (verb === "businesses") {
2080
+ const r2 = await listBusinesses(base2);
2081
+ if (!r2.ok) {
2082
+ line(c.red(` \u2717 ${r2.error}`));
2083
+ return 1;
2084
+ }
2085
+ if (wantsJson) {
2086
+ stdout3.write(JSON.stringify(r2.businesses ?? [], null, 2) + "\n");
2087
+ return 0;
2088
+ }
2089
+ const rows = r2.businesses ?? [];
2090
+ if (!rows.length) {
2091
+ line(c.dim(" no businesses yet \u2014 draw a launch plan (`wholestack launch`) or build an app."));
2092
+ return 0;
2093
+ }
2094
+ line();
2095
+ for (const b of rows) {
2096
+ const id = String(b.id ?? "");
2097
+ const name = typeof b.name === "string" ? b.name : typeof b.brandName === "string" ? b.brandName : "";
2098
+ line(" " + c.cyan(id) + (name ? " " + c.bold(name) : ""));
2099
+ }
2100
+ line();
2101
+ return 0;
2102
+ }
2103
+ const buildId = (firstArg ?? readProjectLink(".")?.buildId)?.trim();
2104
+ const r = await getBusiness(base2, buildId ? { buildId } : {});
2105
+ if (!r.ok) {
2106
+ line(c.red(` \u2717 ${r.error}`));
2107
+ return 1;
2108
+ }
2109
+ stdout3.write(JSON.stringify({ business: r.business, readiness: r.readiness }, null, 2) + "\n");
1119
2110
  return 0;
1120
2111
  }
1121
2112
  var EMPTY_PLUGINS = {
@@ -1144,10 +2135,26 @@ async function main() {
1144
2135
  const rawArgs = argv.slice(2);
1145
2136
  const loggedIn = await runLogin(rawArgs);
1146
2137
  if (loggedIn !== null) exit(loggedIn);
2138
+ const gh = await runGithubSubcommand(rawArgs);
2139
+ if (gh !== null) exit(gh);
2140
+ const provePath = await runProveSubcommand(rawArgs);
2141
+ if (provePath !== null) exit(provePath);
1147
2142
  const sub = await runSecuritySubcommand(rawArgs);
1148
2143
  if (sub !== null) exit(sub);
1149
2144
  const proj = await runProjectSubcommand(rawArgs);
1150
2145
  if (proj !== null) exit(proj);
2146
+ const ship = await runShipSubcommand(rawArgs);
2147
+ if (ship !== null) exit(ship);
2148
+ const launch = await runLaunchSubcommand(rawArgs);
2149
+ if (launch !== null) exit(launch);
2150
+ const hq = await runHqSubcommand(rawArgs);
2151
+ if (hq !== null) exit(hq);
2152
+ const eyes = await runEyesSubcommand(rawArgs);
2153
+ if (eyes !== null) exit(eyes);
2154
+ const ops = await runOpsSubcommand(rawArgs);
2155
+ if (ops !== null) exit(ops);
2156
+ const skillsSub = await runSkillsSubcommand(rawArgs);
2157
+ if (skillsSub !== null) exit(skillsSub);
1151
2158
  const args = parse(rawArgs);
1152
2159
  if (args.version) {
1153
2160
  line(`wholestack ${VERSION}`);
@@ -1161,9 +2168,9 @@ async function main() {
1161
2168
  let token = process.env.ZETA_API_KEY;
1162
2169
  let access = await verifyAccess(webBaseUrl(), token);
1163
2170
  if (!access.active && stdin.isTTY) {
1164
- const rl = createInterface({ input: stdin, output: stdout });
2171
+ const rl = createInterface({ input: stdin, output: stdout3 });
1165
2172
  const ans = (await rl.question(
1166
- "\n " + (token ? c.dim("No active plan on this account. ") + "Log in to a different account? " : "Log in now? ") + c.dim("[Y/n] ")
2173
+ "\n " + (token ? c.dim("Token invalid or expired. ") + "Log in again? " : "Log in now? ") + c.dim("[Y/n] ")
1167
2174
  )).trim().toLowerCase();
1168
2175
  rl.close();
1169
2176
  if (ans === "" || ans === "y" || ans === "yes") {
@@ -1179,6 +2186,16 @@ async function main() {
1179
2186
  showPaywall(Boolean(token), webBaseUrl());
1180
2187
  exit(1);
1181
2188
  }
2189
+ if (!access.paid && access.freeDeliveries) {
2190
+ const fd = access.freeDeliveries;
2191
+ line(
2192
+ c.dim(
2193
+ ` free plan \xB7 ${fd.remaining}/${fd.limit} delivered apps left this month \xB7 `
2194
+ ) + c.cyan(`${webBaseUrl().replace(/\/$/, "")}/pricing`)
2195
+ );
2196
+ } else if (access.paid && access.tier) {
2197
+ line(c.dim(` ${access.tier} plan`));
2198
+ }
1182
2199
  }
1183
2200
  let modelKey;
1184
2201
  try {
@@ -1194,9 +2211,19 @@ async function main() {
1194
2211
  line(c.red(e.message));
1195
2212
  exit(1);
1196
2213
  }
2214
+ if (args.model) {
2215
+ escalationBus.lock();
2216
+ } else {
2217
+ const firstPrompt = args.prompt.length > 0 ? args.prompt : void 0;
2218
+ const boot = pickBootModel(firstPrompt);
2219
+ if (boot && boot.key !== modelKey) {
2220
+ modelKey = boot.key;
2221
+ model = resolveModel(modelKey);
2222
+ }
2223
+ }
1197
2224
  const isTty = !!stdin.isTTY;
1198
2225
  const oneShot = args.prompt.length > 0;
1199
- const workdir = cwd();
2226
+ const workdir = cwd2();
1200
2227
  const persistedMode = isPermissionMode(
1201
2228
  process.env.ZETA_PERMISSION_MODE
1202
2229
  ) ? process.env.ZETA_PERMISSION_MODE : void 0;
@@ -1204,7 +2231,9 @@ async function main() {
1204
2231
  const mode = args.plan ? "plan" : args.mode ? args.mode : args.yes ? "yolo" : persistedMode ?? (isTty ? "yolo" : "acceptEdits");
1205
2232
  const memory = await loadProjectMemory(workdir);
1206
2233
  const plugins = args.noPlugins ? EMPTY_PLUGINS : loadPlugins(workdir);
1207
- const memoryText = [memory.text, plugins.systemText].filter(Boolean).join("\n\n") || void 0;
2234
+ const skills = SkillRegistry.load(workdir);
2235
+ const learned = loadLearnedMemory({ scope: basename2(workdir) });
2236
+ const memoryText = [memory.text, learned.text, plugins.systemText, skills.promptHint()].filter(Boolean).join("\n\n") || void 0;
1208
2237
  const mcp = await loadMcpTools({
1209
2238
  cwd: workdir,
1210
2239
  only: args.mcp,
@@ -1236,7 +2265,7 @@ async function main() {
1236
2265
  },
1237
2266
  // Starship-style context segments above the prompt box.
1238
2267
  contextBar: () => {
1239
- const seg = [c.cyan(modelLabel(modelKey)), c.dim(basename(workdir))];
2268
+ const seg = [c.cyan(modelLabel(modelKey)), c.dim(basename2(workdir))];
1240
2269
  const br = gitBranch(workdir);
1241
2270
  if (br) seg.push(c.dim("\u2387 " + br));
1242
2271
  seg.push(mode === "yolo" ? c.yellow(mode) : c.dim(mode));
@@ -1258,7 +2287,21 @@ async function main() {
1258
2287
  ...buildWebTools(),
1259
2288
  ...mcp.tools,
1260
2289
  // Parallel read-only sub-agents (needs the model handle the base tools lack).
1261
- ...buildSubagentTool({ model, modelKey, ctx: toolCtx })
2290
+ ...buildSubagentTool({ model, modelKey, ctx: toolCtx }),
2291
+ // Parallel WRITABLE workers in isolated git worktrees (review-then-merge diffs).
2292
+ ...buildWorkersTool({ model, modelKey, ctx: toolCtx }),
2293
+ // Self-verify + heal: run the web2 ShipGate (prove_app) + the app's own checks (verify_build).
2294
+ ...buildVerifyTools({ ctx: toolCtx }),
2295
+ // Eyes — open a running app in a real browser and report what it sees.
2296
+ ...buildEyesTool({ ctx: toolCtx }),
2297
+ // Living ops — operate the app after ship (health/deploy/metrics/errors).
2298
+ ...buildOpsTools({ ctx: toolCtx }),
2299
+ // Cross-session learned memory (remember / recall).
2300
+ ...buildLearnedMemoryTools({ ctx: toolCtx }),
2301
+ // Composable skills (list_skills / use_skill) from the boot-time registry.
2302
+ ...buildSkillTools({ registry: skills }),
2303
+ // Auto model-routing: the escalate_model tool posts requests the loop drains.
2304
+ ...buildEscalateTool({ ctx: toolCtx, currentKey: modelKey, bus: escalationBus })
1262
2305
  };
1263
2306
  let session;
1264
2307
  let resumedMessages = null;
@@ -1286,7 +2329,7 @@ async function main() {
1286
2329
  persona: args.persona,
1287
2330
  thinking,
1288
2331
  yolo: mode === "yolo",
1289
- maxSteps: 16,
2332
+ maxSteps: 50,
1290
2333
  contextWindow: modelContextWindow(modelKey),
1291
2334
  session
1292
2335
  });
@@ -1337,6 +2380,19 @@ ${m.context}` : args.prompt;
1337
2380
  const runTurn = async (text, images) => {
1338
2381
  const t0 = Date.now();
1339
2382
  await ic.runInterruptible((sig) => agent.send(text, sig, images));
2383
+ const pending = escalationBus.drain();
2384
+ if (pending && pending.key !== modelKey) {
2385
+ try {
2386
+ modelKey = pending.key;
2387
+ agent.setModel(resolveModel(modelKey), modelKey, modelContextWindow(modelKey));
2388
+ agent.setThinking(supportsThinking(modelKey) ? args.think ?? false : false);
2389
+ line(
2390
+ " " + c.green(`\u2192 ${modelLabel(modelKey)}`) + c.dim(` \xB7 auto-route: ${pending.reason}`) + "\n"
2391
+ );
2392
+ } catch (e) {
2393
+ line(" " + c.red(e.message));
2394
+ }
2395
+ }
1340
2396
  statusLine({
1341
2397
  model: modelLabel(modelKey),
1342
2398
  tokens: agent.usage.outputTokens,
@@ -1373,6 +2429,7 @@ ${m.context}` : args.prompt;
1373
2429
  const m = resolveModel(res.modelKey);
1374
2430
  modelKey = res.modelKey;
1375
2431
  agent.setModel(m, modelKey, modelContextWindow(modelKey));
2432
+ escalationBus.lock();
1376
2433
  agent.setThinking(supportsThinking(modelKey) ? args.think ?? false : false);
1377
2434
  line(
1378
2435
  " " + c.green(`\u2192 ${modelLabel(modelKey)}`) + (agent.thinkingOn ? c.dim(" \xB7 thinking on") : "") + "\n"