wholestack 0.5.5 → 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,15 +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,
22
+ collectLocalFiles,
23
+ deployBuild,
24
+ drawCanvas,
25
+ escalationBus,
26
+ fetchGithubInstallations,
27
+ fetchGithubRepos,
28
+ generateLegal,
29
+ getBusiness,
30
+ getLaunchKit,
31
+ inspectUrl,
15
32
  isPermissionMode,
16
33
  killRunningApps,
17
34
  line,
35
+ listBusinesses,
36
+ listProjects,
18
37
  loadHookFiles,
38
+ loadLearnedMemory,
19
39
  loadMcpTools,
20
40
  loadPlugins,
21
41
  loadProjectMemory,
@@ -23,21 +43,43 @@ import {
23
43
  mergeHookSets,
24
44
  modelContextWindow,
25
45
  modelLabel,
46
+ openBuildInEditor,
47
+ pickBootModel,
48
+ pickGithubRepoInteractive,
49
+ promoteBuild,
50
+ proveLocalRepo,
51
+ pullWorkingTree,
52
+ pushWorkingTree,
53
+ readMetrics,
54
+ readProjectLink,
55
+ readSourceFile,
56
+ rememberLesson,
57
+ renderGtmPlanLines,
58
+ renderLaunchKitLines,
59
+ renderMetricsLines,
26
60
  resolveModel,
27
61
  resolveModelKey,
62
+ rollbackDeployment,
28
63
  runProver,
64
+ runRepoAuditStream,
29
65
  saveKey,
66
+ searchLessons,
30
67
  stateDir,
31
68
  statusLine,
32
69
  supportsThinking,
33
70
  tasks,
71
+ toolLine,
34
72
  userBox,
35
- visionCapable
36
- } from "./chunk-7NZ77Q7Q.js";
73
+ visionCapable,
74
+ waitlistCount,
75
+ webBaseUrl,
76
+ writeProjectLink,
77
+ writeWorkingTree
78
+ } from "./chunk-TLYCOIEA.js";
37
79
 
38
80
  // src/cli.ts
39
81
  import { createInterface } from "readline/promises";
40
- import { stdin, stdout, argv, exit, cwd } from "process";
82
+ import { stdin, stdout as stdout3, argv, exit, cwd as cwd2 } from "process";
41
83
 
42
84
  // src/cli-login.ts
43
85
  import { createServer } from "http";
@@ -290,7 +332,7 @@ async function renderTree(absDir, relDir) {
290
332
  return `${relDir || "."}/ (directory tree)
291
333
  ${lines.join("\n")}`;
292
334
  }
293
- async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET) {
335
+ async function expandMentions(message, cwd3, tokenBudget = DEFAULT_TOKEN_BUDGET) {
294
336
  const mentions = extractMentions(message);
295
337
  if (mentions.length === 0) return { context: "", notice: null };
296
338
  const blocks = [];
@@ -300,7 +342,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
300
342
  for (const mention of mentions) {
301
343
  if (spent >= tokenBudget) break;
302
344
  const raw = mention.startsWith("~/") ? join(process.env.HOME ?? "", mention.slice(2)) : mention;
303
- const direct = isAbsolute(raw) ? raw : resolve(cwd2, raw);
345
+ const direct = isAbsolute(raw) ? raw : resolve(cwd3, raw);
304
346
  let isDir = false;
305
347
  try {
306
348
  isDir = (await stat(direct)).isDirectory();
@@ -308,7 +350,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
308
350
  isDir = false;
309
351
  }
310
352
  if (isDir) {
311
- const rel = relative(cwd2, direct);
353
+ const rel = relative(cwd3, direct);
312
354
  const tree = await renderTree(direct, rel);
313
355
  const cost = estTokens(tree);
314
356
  if (spent + cost <= tokenBudget) {
@@ -321,7 +363,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
321
363
  let files = [];
322
364
  try {
323
365
  files = await fg(raw, {
324
- cwd: cwd2,
366
+ cwd: cwd3,
325
367
  ignore: IGNORE,
326
368
  onlyFiles: true,
327
369
  dot: true,
@@ -333,7 +375,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
333
375
  }
334
376
  if (files.length === 0) {
335
377
  try {
336
- if ((await stat(direct)).isFile()) files = [relative(cwd2, direct)];
378
+ if ((await stat(direct)).isFile()) files = [relative(cwd3, direct)];
337
379
  } catch {
338
380
  }
339
381
  }
@@ -344,7 +386,7 @@ async function expandMentions(message, cwd2, tokenBudget = DEFAULT_TOKEN_BUDGET)
344
386
  seen.add(rel);
345
387
  let buf;
346
388
  try {
347
- buf = await readFile2(resolve(cwd2, rel), "utf8");
389
+ buf = await readFile2(resolve(cwd3, rel), "utf8");
348
390
  } catch {
349
391
  continue;
350
392
  }
@@ -424,11 +466,528 @@ function buildSubagentTool(deps) {
424
466
  };
425
467
  }
426
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
+
427
986
  // src/images.ts
428
987
  import { readFile as readFile3, stat as stat2 } from "fs/promises";
429
- 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";
430
989
  import { execFile } from "child_process";
431
- import { tmpdir, platform as platform2 } from "os";
990
+ import { tmpdir as tmpdir2, platform as platform2 } from "os";
432
991
  var EXT_TO_MEDIA = {
433
992
  ".png": "image/png",
434
993
  ".jpg": "image/jpeg",
@@ -447,7 +1006,7 @@ function run(cmd, args) {
447
1006
  });
448
1007
  }
449
1008
  async function grabClipboardImage() {
450
- 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`);
451
1010
  const os = platform2();
452
1011
  if (os === "darwin") {
453
1012
  const script = `set thePath to "${out}"
@@ -481,7 +1040,7 @@ function imageTokens(message) {
481
1040
  for (let m = re.exec(message); m; m = re.exec(message)) out.add(m[1]);
482
1041
  return [...out];
483
1042
  }
484
- async function scanImages(message, cwd2) {
1043
+ async function scanImages(message, cwd3) {
485
1044
  const tokens = imageTokens(message);
486
1045
  const images = [];
487
1046
  const skipped = [];
@@ -510,8 +1069,8 @@ async function scanImages(message, cwd2) {
510
1069
  }
511
1070
  }
512
1071
  for (const tok of tokens) {
513
- const raw = tok.startsWith("~/") ? join2(process.env.HOME ?? "", tok.slice(2)) : tok;
514
- 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);
515
1074
  const mediaType = EXT_TO_MEDIA[extname(abs).toLowerCase()];
516
1075
  if (!mediaType) continue;
517
1076
  let bytes;
@@ -522,7 +1081,7 @@ async function scanImages(message, cwd2) {
522
1081
  } catch {
523
1082
  continue;
524
1083
  }
525
- const label = isAbsolute2(raw) ? raw : relative2(cwd2, abs);
1084
+ const label = isAbsolute2(raw) ? raw : relative2(cwd3, abs);
526
1085
  if (bytes > PER_IMAGE_BYTES || total + bytes > TOTAL_BYTES) {
527
1086
  skipped.push(`${label} (${Math.round(bytes / 1024)}KB \u2014 over the size cap)`);
528
1087
  continue;
@@ -656,21 +1215,21 @@ ${text}`;
656
1215
  ${body}
657
1216
  `;
658
1217
  }
659
- function writeTranscript(messages, meta, file, cwd2) {
660
- const abs = resolve3(cwd2, file);
1218
+ function writeTranscript(messages, meta, file, cwd3) {
1219
+ const abs = resolve3(cwd3, file);
661
1220
  writeFileSync(abs, renderTranscript(messages, meta), "utf8");
662
1221
  return abs;
663
1222
  }
664
1223
 
665
1224
  // src/cli.ts
666
1225
  import { existsSync as existsSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
667
- import { join as join5 } from "path";
1226
+ import { join as join6 } from "path";
668
1227
 
669
1228
  // src/update-check.ts
670
1229
  import { existsSync, mkdirSync, readFileSync, writeFileSync as writeFileSync2 } from "fs";
671
- import { join as join3 } from "path";
1230
+ import { join as join4 } from "path";
672
1231
  var PKG = "wholestack";
673
- var CACHE = join3(stateDir(), "update-check.json");
1232
+ var CACHE = join4(stateDir(), "update-check.json");
674
1233
  var DAY_MS = 24 * 60 * 60 * 1e3;
675
1234
  function readCache() {
676
1235
  try {
@@ -724,8 +1283,8 @@ async function checkForUpdate(current) {
724
1283
 
725
1284
  // src/access.ts
726
1285
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync3 } from "fs";
727
- import { join as join4 } from "path";
728
- var CACHE2 = join4(stateDir(), "access.json");
1286
+ import { join as join5 } from "path";
1287
+ var CACHE2 = join5(stateDir(), "access.json");
729
1288
  var TTL_MS = 60 * 60 * 1e3;
730
1289
  function readCache2() {
731
1290
  try {
@@ -748,10 +1307,18 @@ function clearAccessCache() {
748
1307
  }
749
1308
  }
750
1309
  async function verifyAccess(webUrl, token, force = false) {
751
- 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
+ }
752
1313
  const cached = force ? null : readCache2();
753
1314
  if (cached && Date.now() - cached.checkedAt < TTL_MS) {
754
- 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
+ };
755
1322
  }
756
1323
  try {
757
1324
  const ctrl = new AbortController();
@@ -762,18 +1329,44 @@ async function verifyAccess(webUrl, token, force = false) {
762
1329
  });
763
1330
  clearTimeout(t);
764
1331
  if (res.status >= 500) {
765
- 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
+ };
766
1339
  }
767
1340
  if (!res.ok) {
768
- writeCache2({ checkedAt: Date.now(), active: false, tier: null });
769
- 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 };
770
1349
  }
771
1350
  const json = await res.json();
772
1351
  const active = Boolean(json.active);
773
- writeCache2({ checkedAt: Date.now(), active, tier: json.tier ?? null });
774
- 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 };
775
1362
  } catch {
776
- 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
+ };
777
1370
  }
778
1371
  }
779
1372
  function showPaywall(loggedIn, webUrl) {
@@ -781,36 +1374,36 @@ function showPaywall(loggedIn, webUrl) {
781
1374
  line();
782
1375
  line(" " + c.cyan("\u256D" + "\u2500".repeat(w) + "\u256E"));
783
1376
  const row = (s) => line(" " + c.cyan("\u2502") + " " + s.padEnd(w - 2) + " " + c.cyan("\u2502"));
784
- row(c.bold("zeta needs a subscription"));
1377
+ row(c.bold("wholestack needs you signed in"));
785
1378
  row("");
786
1379
  if (!loggedIn) {
787
1380
  row(c.dim("You're not signed in."));
788
- row("Run " + c.cyan("zeta login") + c.dim(" then subscribe."));
1381
+ row("Run " + c.cyan("wholestack login") + c.dim(" to start."));
789
1382
  } else {
790
- row(c.dim("No active paid plan on this account."));
791
- 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."));
792
1385
  }
793
1386
  row("");
794
- 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`));
795
1388
  line(" " + c.cyan("\u2570" + "\u2500".repeat(w) + "\u256F"));
796
1389
  line();
797
1390
  }
798
1391
 
799
1392
  // src/cli.ts
800
1393
  import { readFileSync as readFileSync3 } from "fs";
801
- import { basename, dirname as dirname2 } from "path";
1394
+ import { basename as basename2, dirname as dirname2 } from "path";
802
1395
  import { fileURLToPath } from "url";
803
1396
  var VERSION = (() => {
804
1397
  try {
805
1398
  const here = dirname2(fileURLToPath(import.meta.url));
806
- return JSON.parse(readFileSync3(join5(here, "..", "package.json"), "utf8")).version;
1399
+ return JSON.parse(readFileSync3(join6(here, "..", "package.json"), "utf8")).version;
807
1400
  } catch {
808
1401
  return "0.0.0";
809
1402
  }
810
1403
  })();
811
1404
  function gitBranch(dir) {
812
1405
  try {
813
- const head = readFileSync3(join5(dir, ".git", "HEAD"), "utf8").trim();
1406
+ const head = readFileSync3(join6(dir, ".git", "HEAD"), "utf8").trim();
814
1407
  const m = head.match(/ref:\s*refs\/heads\/(.+)$/);
815
1408
  return m ? m[1] : head.slice(0, 7);
816
1409
  } catch {
@@ -871,13 +1464,45 @@ ${c.bold("Usage")}
871
1464
  wholestack "build me a todo app" run one prompt and exit
872
1465
  wholestack -m zeta-g1-max "..." drive it with the most capable brain
873
1466
 
1467
+ ${c.bold("Projects")} ${c.dim("(one spine across terminal, dashboard and the web IDE)")}
1468
+ wholestack projects list your platform projects
1469
+ wholestack pull <id> [dir] download a project's working tree
1470
+ wholestack push [dir] sync local edits back to the web IDE
1471
+ wholestack open [buildId] open a generated build in the web IDE
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
+
874
1494
  ${c.bold("Web3 security")} ${c.dim("(forge \xB7 slither \xB7 firewall \xB7 halmos \xB7 signed cert)")}
875
- 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
876
1497
  wholestack prove <dir> <Contract> --property <k> prove one invariant
877
1498
  wholestack verify <dir> <Contract> --cert c.json passthrough + signed certificate
878
1499
  ${c.dim("kinds: reentrancy-safety, access-control, conservation, no-value-extraction, \u2026")}
879
1500
  wholestack --nzt48 "audit ./contracts" NZT-48 web3 special-ops persona
880
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
+
881
1506
  ${c.bold("Session")}
882
1507
  -m, --model <key> zeta-g1-lite | zeta-g1 | zeta-g1-max | vision
883
1508
  (lite = light \xB7 g1 = default \xB7 max = most capable \xB7 vision = sees images)
@@ -915,10 +1540,218 @@ ${c.bold("In a session")} ${c.dim("(type /help for the full list)")}
915
1540
  ${c.dim("@shot.png \xB7 @clip attach an image / paste from clipboard (auto-switches to Vision)")}
916
1541
  ${c.dim("@https://\u2026 fetch a web page and attach its text to the turn")}
917
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
+ }
918
1748
  async function runSecuritySubcommand(raw) {
919
1749
  const verb = raw[0];
920
1750
  if (verb !== "audit" && verb !== "prove" && verb !== "verify") return null;
921
1751
  const rest = raw.slice(1);
1752
+ if (verb === "audit" && rest[0] === "repo") {
1753
+ return runAuditRepoSubcommand(rest.slice(1));
1754
+ }
922
1755
  const proverArgs = verb === "verify" && rest[0]?.endsWith(".json") ? ["verify", ...rest] : [...rest];
923
1756
  if (verb === "prove" && !proverArgs.includes("--property")) {
924
1757
  line(c.red(" prove needs --property <kind>"));
@@ -928,7 +1761,7 @@ async function runSecuritySubcommand(raw) {
928
1761
  line();
929
1762
  line(c.cyan(` \u259F ShipGate ${verb}`) + c.dim(" \xB7 forge \xB7 slither \xB7 firewall \xB7 halmos"));
930
1763
  line();
931
- const r = await runProver(proverArgs, { cwd: cwd(), inherit: true });
1764
+ const r = await runProver(proverArgs, { cwd: cwd2(), inherit: true });
932
1765
  if (r.missing) {
933
1766
  line(c.red(` ${r.out}`));
934
1767
  return 1;
@@ -940,7 +1773,7 @@ async function runSecuritySubcommand(raw) {
940
1773
  function maybeYoloNotice() {
941
1774
  try {
942
1775
  const dir = stateDir();
943
- const marker = join5(dir, "yolo-notice-seen");
1776
+ const marker = join6(dir, "yolo-notice-seen");
944
1777
  if (existsSync2(marker)) return;
945
1778
  line(
946
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"
@@ -950,9 +1783,6 @@ function maybeYoloNotice() {
950
1783
  } catch {
951
1784
  }
952
1785
  }
953
- function webBaseUrl() {
954
- return process.env.ZETA_WEB_URL?.trim() || process.env.ZETA_API_URL?.trim() || "https://wholestack.ai";
955
- }
956
1786
  async function runLogin(raw) {
957
1787
  if (raw[0] !== "login" && raw[0] !== "logout") return null;
958
1788
  const rest = raw.slice(1);
@@ -1004,7 +1834,7 @@ async function runLogin(raw) {
1004
1834
  line(c.red(" usage: wholestack login --member <key> | --brain <key> | --build <key>"));
1005
1835
  return 1;
1006
1836
  }
1007
- const rl = createInterface({ input: stdin, output: stdout });
1837
+ const rl = createInterface({ input: stdin, output: stdout3 });
1008
1838
  if (!name) {
1009
1839
  const which = (await rl.question(c.cyan(" key for [member/brain/engine]: "))).trim().toLowerCase();
1010
1840
  name = which.startsWith("m") ? "ZETA_API_KEY" : "CEREBRAS_API_KEY";
@@ -1023,6 +1853,262 @@ async function runLogin(raw) {
1023
1853
  line(c.dim(" now just run `wholestack`."));
1024
1854
  return 0;
1025
1855
  }
1856
+ async function runProjectSubcommand(raw) {
1857
+ const verb = raw[0];
1858
+ if (verb !== "projects" && verb !== "pull" && verb !== "push" && verb !== "open") return null;
1859
+ const base2 = webBaseUrl();
1860
+ if (verb === "projects") {
1861
+ const r2 = await listProjects(base2);
1862
+ if (!r2.ok) {
1863
+ line(c.red(` ${r2.error}`));
1864
+ return 1;
1865
+ }
1866
+ if (!r2.projects?.length) {
1867
+ line(c.dim(' no projects yet \u2014 build something (`wholestack "an idea"`) or create one in the web IDE.'));
1868
+ return 0;
1869
+ }
1870
+ line();
1871
+ for (const p of r2.projects) {
1872
+ const when = p.updatedAt ? new Date(p.updatedAt).toISOString().slice(0, 10) : "";
1873
+ line(" " + c.cyan(p.id) + " " + c.bold(p.name) + (when ? c.dim(` \xB7 ${when}`) : ""));
1874
+ if (p.description) line(" " + c.dim(` ${p.description}`));
1875
+ }
1876
+ line();
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>`));
1878
+ return 0;
1879
+ }
1880
+ if (verb === "pull") {
1881
+ const projectId = raw[1];
1882
+ if (!projectId) {
1883
+ line(c.red(" usage: wholestack pull <projectId> [dir]"));
1884
+ return 1;
1885
+ }
1886
+ const r2 = await pullWorkingTree(base2, projectId);
1887
+ if (!r2.ok) {
1888
+ line(c.red(` ${r2.error}`));
1889
+ return 1;
1890
+ }
1891
+ const dest = raw[2] ?? (r2.name ? r2.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") : projectId);
1892
+ const written = writeWorkingTree(dest, r2.files ?? []);
1893
+ writeProjectLink(dest, { projectId });
1894
+ line(c.green(` \u2713 pulled ${written} files`) + c.dim(` \u2192 ${dest}/`));
1895
+ line(c.dim(" edit locally, then ") + c.cyan(`wholestack push ${dest === "." ? "" : dest}`).trimEnd() + c.dim(" to sync back to the web IDE."));
1896
+ return 0;
1897
+ }
1898
+ if (verb === "push") {
1899
+ const dir = raw[1] ?? ".";
1900
+ const link = readProjectLink(dir);
1901
+ const flagIdx = raw.indexOf("--project");
1902
+ const projectId = (flagIdx >= 0 ? raw[flagIdx + 1] : void 0) ?? link?.projectId;
1903
+ if (!projectId) {
1904
+ line(c.red(" no linked project \u2014 pull first, or pass --project <id>."));
1905
+ return 1;
1906
+ }
1907
+ const { files, skipped } = await collectLocalFiles(dir);
1908
+ if (!files.length) {
1909
+ line(c.red(" nothing to push (no readable text files found)."));
1910
+ return 1;
1911
+ }
1912
+ const r2 = await pushWorkingTree(base2, projectId, files);
1913
+ if (!r2.ok) {
1914
+ line(c.red(` ${r2.error}`));
1915
+ return 1;
1916
+ }
1917
+ writeProjectLink(dir, { projectId });
1918
+ line(c.green(` \u2713 pushed ${files.length} files`) + c.dim(skipped ? ` (${skipped} skipped: binary/oversize)` : ""));
1919
+ line(c.dim(" open it: ") + c.cyan(`${base2}/ide/studio?projectId=${projectId}`));
1920
+ return 0;
1921
+ }
1922
+ const buildId = raw[1] ?? readProjectLink(".")?.buildId;
1923
+ if (!buildId) {
1924
+ line(c.red(" usage: wholestack open <buildId> (or run it inside a delivered app dir)"));
1925
+ return 1;
1926
+ }
1927
+ const r = await openBuildInEditor(base2, buildId);
1928
+ if (!r.ok) {
1929
+ line(c.red(` ${r.error}`));
1930
+ return 1;
1931
+ }
1932
+ if (r.projectId) writeProjectLink(".", { buildId, projectId: r.projectId });
1933
+ line(c.green(" \u2713 project ready in the web IDE"));
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");
2110
+ return 0;
2111
+ }
1026
2112
  var EMPTY_PLUGINS = {
1027
2113
  plugins: [],
1028
2114
  systemText: "",
@@ -1049,8 +2135,26 @@ async function main() {
1049
2135
  const rawArgs = argv.slice(2);
1050
2136
  const loggedIn = await runLogin(rawArgs);
1051
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);
1052
2142
  const sub = await runSecuritySubcommand(rawArgs);
1053
2143
  if (sub !== null) exit(sub);
2144
+ const proj = await runProjectSubcommand(rawArgs);
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);
1054
2158
  const args = parse(rawArgs);
1055
2159
  if (args.version) {
1056
2160
  line(`wholestack ${VERSION}`);
@@ -1064,9 +2168,9 @@ async function main() {
1064
2168
  let token = process.env.ZETA_API_KEY;
1065
2169
  let access = await verifyAccess(webBaseUrl(), token);
1066
2170
  if (!access.active && stdin.isTTY) {
1067
- const rl = createInterface({ input: stdin, output: stdout });
2171
+ const rl = createInterface({ input: stdin, output: stdout3 });
1068
2172
  const ans = (await rl.question(
1069
- "\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] ")
1070
2174
  )).trim().toLowerCase();
1071
2175
  rl.close();
1072
2176
  if (ans === "" || ans === "y" || ans === "yes") {
@@ -1082,6 +2186,16 @@ async function main() {
1082
2186
  showPaywall(Boolean(token), webBaseUrl());
1083
2187
  exit(1);
1084
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
+ }
1085
2199
  }
1086
2200
  let modelKey;
1087
2201
  try {
@@ -1097,9 +2211,19 @@ async function main() {
1097
2211
  line(c.red(e.message));
1098
2212
  exit(1);
1099
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
+ }
1100
2224
  const isTty = !!stdin.isTTY;
1101
2225
  const oneShot = args.prompt.length > 0;
1102
- const workdir = cwd();
2226
+ const workdir = cwd2();
1103
2227
  const persistedMode = isPermissionMode(
1104
2228
  process.env.ZETA_PERMISSION_MODE
1105
2229
  ) ? process.env.ZETA_PERMISSION_MODE : void 0;
@@ -1107,7 +2231,9 @@ async function main() {
1107
2231
  const mode = args.plan ? "plan" : args.mode ? args.mode : args.yes ? "yolo" : persistedMode ?? (isTty ? "yolo" : "acceptEdits");
1108
2232
  const memory = await loadProjectMemory(workdir);
1109
2233
  const plugins = args.noPlugins ? EMPTY_PLUGINS : loadPlugins(workdir);
1110
- 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;
1111
2237
  const mcp = await loadMcpTools({
1112
2238
  cwd: workdir,
1113
2239
  only: args.mcp,
@@ -1139,7 +2265,7 @@ async function main() {
1139
2265
  },
1140
2266
  // Starship-style context segments above the prompt box.
1141
2267
  contextBar: () => {
1142
- const seg = [c.cyan(modelLabel(modelKey)), c.dim(basename(workdir))];
2268
+ const seg = [c.cyan(modelLabel(modelKey)), c.dim(basename2(workdir))];
1143
2269
  const br = gitBranch(workdir);
1144
2270
  if (br) seg.push(c.dim("\u2387 " + br));
1145
2271
  seg.push(mode === "yolo" ? c.yellow(mode) : c.dim(mode));
@@ -1161,7 +2287,21 @@ async function main() {
1161
2287
  ...buildWebTools(),
1162
2288
  ...mcp.tools,
1163
2289
  // Parallel read-only sub-agents (needs the model handle the base tools lack).
1164
- ...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 })
1165
2305
  };
1166
2306
  let session;
1167
2307
  let resumedMessages = null;
@@ -1189,7 +2329,7 @@ async function main() {
1189
2329
  persona: args.persona,
1190
2330
  thinking,
1191
2331
  yolo: mode === "yolo",
1192
- maxSteps: 16,
2332
+ maxSteps: 50,
1193
2333
  contextWindow: modelContextWindow(modelKey),
1194
2334
  session
1195
2335
  });
@@ -1240,6 +2380,19 @@ ${m.context}` : args.prompt;
1240
2380
  const runTurn = async (text, images) => {
1241
2381
  const t0 = Date.now();
1242
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
+ }
1243
2396
  statusLine({
1244
2397
  model: modelLabel(modelKey),
1245
2398
  tokens: agent.usage.outputTokens,
@@ -1276,6 +2429,7 @@ ${m.context}` : args.prompt;
1276
2429
  const m = resolveModel(res.modelKey);
1277
2430
  modelKey = res.modelKey;
1278
2431
  agent.setModel(m, modelKey, modelContextWindow(modelKey));
2432
+ escalationBus.lock();
1279
2433
  agent.setThinking(supportsThinking(modelKey) ? args.think ?? false : false);
1280
2434
  line(
1281
2435
  " " + c.green(`\u2192 ${modelLabel(modelKey)}`) + (agent.thinkingOn ? c.dim(" \xB7 thinking on") : "") + "\n"