aui-agent-builder 0.3.77 → 0.3.79

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.
@@ -11,7 +11,7 @@ import { findAuiFiles, parseAuiFile } from "../utils/index.js";
11
11
  import { validate } from "./validate.js";
12
12
  import { getTracer, SpanStatusCode, setUserContext } from "../telemetry.js";
13
13
  import { getItemLevelDiff } from "../utils/git.js";
14
- import { AuthenticationError, ConfigError, ValidationError } from "../errors/index.js";
14
+ import { AuthenticationError, CLIError, ConfigError, ValidationError } from "../errors/index.js";
15
15
  import { StatusLine, Spinner, ErrorDisplay, Hint, } from "../ui/components/index.js";
16
16
  import { colors, icons } from "../ui/theme.js";
17
17
  import { PushFileSummary, PushChangesView, PushTaskLine, PushFinalSummary, } from "../ui/views/PushView.js";
@@ -439,8 +439,6 @@ async function _push(pushSpan, agentCode, options = {}) {
439
439
  log(_jsx(StatusLine, { kind: "info", label: `Scope level: ${effectiveScopeLevel} (from ${options.scopeLevel ? "flag" : ".auirc"})` }));
440
440
  }
441
441
  const agentSettingsParams = await resolveAgentSettingsParams(config, projectConfig, session, projectRoot, effectiveScopeLevel);
442
- if (!agentSettingsParams)
443
- return;
444
442
  const client = new AUIClient({
445
443
  baseUrl: config.apiUrl,
446
444
  authToken: config.authToken,
@@ -466,9 +464,6 @@ async function _push(pushSpan, agentCode, options = {}) {
466
464
  let prePushDraft = null;
467
465
  if (projectConfig.version_id || options.versionId) {
468
466
  prePushDraft = await resolveVersionDraft(config, projectConfig, session, options.versionId);
469
- if (!prePushDraft) {
470
- return;
471
- }
472
467
  agentSettingsParams.version_id = prePushDraft.versionId;
473
468
  log(_jsx(StatusLine, { kind: "info", label: `Pushing into draft version: ${prePushDraft.label}` }));
474
469
  }
@@ -584,8 +579,11 @@ async function _push(pushSpan, agentCode, options = {}) {
584
579
  }
585
580
  return stepFailed.length === 0 && !authFailed;
586
581
  }
587
- catch {
588
- return false;
582
+ catch (err) {
583
+ // Don't swallow — a thrown error here means the loop body itself
584
+ // (not a per-task failure) blew up. Letting the original `} catch
585
+ // { return false; }` swallow it caused exit 0 with no telemetry.
586
+ throw err;
589
587
  }
590
588
  };
591
589
  const paramTasks = pushTasks.filter((t) => t.type === "put-parameters" ||
@@ -615,6 +613,22 @@ async function _push(pushSpan, agentCode, options = {}) {
615
613
  await pushStep(rulesTasks, "Pushing rules", false);
616
614
  // Auth fallback
617
615
  if (authFailed && authFailedTasks.length > 0 && !savedApiKey) {
616
+ // The auth fallback prompts for an API key. In a non-TTY environment
617
+ // (`agent-builder-bff` E2B sandbox, CI, JSON mode being piped) the
618
+ // inquirer prompt would block forever waiting on stdin and the
619
+ // sandbox would eventually SIGTERM the process — exactly the silent
620
+ // failure mode Aviram and Dor were hitting. Detect that up front
621
+ // and throw a structured AuthenticationError so handleError can
622
+ // print it, emit the JSON envelope, and exit non-zero.
623
+ const isInteractive = !json &&
624
+ process.stdin.isTTY === true &&
625
+ process.stdout.isTTY === true;
626
+ if (!isInteractive) {
627
+ failed += authFailedTasks.length;
628
+ throw new AuthenticationError(`Authentication failed for ${authFailedTasks.length} push task(s); cannot prompt for an API key (non-interactive session).`, {
629
+ suggestion: "Pass --api-key <key>, set AUI_AGENT_TOOLS_API_KEY, or run `aui login` to refresh credentials.",
630
+ });
631
+ }
618
632
  log(_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(StatusLine, { kind: "warning", label: "Authentication failed. Your access token may not have permission." }), _jsx(Hint, { message: "You can provide an API key as a fallback. It will be saved to ~/.aui/agent-settings-key" })] }));
619
633
  const { key } = await inquirer.prompt([
620
634
  {
@@ -675,31 +689,79 @@ async function _push(pushSpan, agentCode, options = {}) {
675
689
  // source of truth for this push. Runs regardless of entity-push outcomes
676
690
  // so the file history is preserved even on partial DB failures.
677
691
  // Files = source of truth; DB updates are best-effort.
692
+ //
693
+ // IMPORTANT: snapshot failures must be loud — they are NOT counted in
694
+ // the entity `failed` counter, so without explicit surfacing here and
695
+ // a thrown CLIError at the end of `_push`, a snapshot failure would
696
+ // be hidden behind the "All N change(s) pushed successfully" summary
697
+ // and the process would exit 0.
698
+ // Retry policy: snapshot upload is the source-of-truth step; transient
699
+ // failures (network blips, 5xx, timeouts) are common with multipart
700
+ // uploads, so we retry up to 3 times with exponential backoff (1s, 2s,
701
+ // 4s) before giving up. 4xx responses are still retried — keeping the
702
+ // policy dumb here mirrors the single retry already in the api-client
703
+ // layer for entity pushes.
678
704
  let snapshotSucceeded = false;
679
705
  let snapshotError;
706
+ let snapshotAttempts = 0;
680
707
  if (prePushDraft) {
681
- const snapshotSpinner = startSpinner("Pushing snapshot (file state)...");
682
- try {
683
- const snapshotResult = await pushSnapshot(client, prePushDraft.agentId, prePushDraft.versionId, projectRoot, fileData);
684
- if (snapshotResult.success) {
685
- snapshotSpinner.succeed(`Snapshot pushed (${fileData.length} file(s))`);
686
- snapshotSucceeded = true;
687
- pushSpan.setAttribute("push.snapshot.success", true);
708
+ const SNAPSHOT_MAX_ATTEMPTS = 4;
709
+ const SNAPSHOT_RETRY_BASE_MS = 1000;
710
+ for (let attempt = 1; attempt <= SNAPSHOT_MAX_ATTEMPTS; attempt++) {
711
+ snapshotAttempts = attempt;
712
+ const label = attempt === 1
713
+ ? "Pushing snapshot (file state)..."
714
+ : `Retrying snapshot push (attempt ${attempt}/${SNAPSHOT_MAX_ATTEMPTS})...`;
715
+ if (json)
716
+ stderrLog(label);
717
+ const snapshotSpinner = json ? null : startSpinner(label);
718
+ let attemptError;
719
+ try {
720
+ const snapshotResult = await pushSnapshot(client, prePushDraft.agentId, prePushDraft.versionId, projectRoot, fileData);
721
+ if (snapshotResult.success) {
722
+ const okMsg = attempt === 1
723
+ ? `Snapshot pushed (${fileData.length} file(s))`
724
+ : `Snapshot pushed (${fileData.length} file(s), attempt ${attempt}/${SNAPSHOT_MAX_ATTEMPTS})`;
725
+ if (snapshotSpinner)
726
+ snapshotSpinner.succeed(okMsg);
727
+ else
728
+ stderrLog(okMsg);
729
+ snapshotSucceeded = true;
730
+ snapshotError = undefined;
731
+ break;
732
+ }
733
+ attemptError = snapshotResult.error || "Unknown snapshot error";
688
734
  }
689
- else {
690
- snapshotSpinner.fail("Snapshot push failed");
691
- snapshotError = snapshotResult.error || "Unknown snapshot error";
692
- pushSpan.setAttribute("push.snapshot.success", false);
693
- pushSpan.setAttribute("push.snapshot.error", snapshotError);
735
+ catch (error) {
736
+ attemptError = error instanceof Error ? error.message : String(error);
694
737
  }
738
+ snapshotError = attemptError;
739
+ const isLast = attempt === SNAPSHOT_MAX_ATTEMPTS;
740
+ const failMsg = isLast
741
+ ? `Snapshot push failed after ${attempt} attempt(s): ${attemptError}`
742
+ : `Snapshot push failed (attempt ${attempt}/${SNAPSHOT_MAX_ATTEMPTS}): ${attemptError}`;
743
+ if (snapshotSpinner)
744
+ snapshotSpinner.fail(failMsg);
745
+ else
746
+ stderrLog(failMsg);
747
+ if (isLast)
748
+ break;
749
+ const delayMs = SNAPSHOT_RETRY_BASE_MS * Math.pow(2, attempt - 1);
750
+ if (json)
751
+ stderrLog(`Waiting ${delayMs}ms before retry...`);
752
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
695
753
  }
696
- catch (error) {
697
- snapshotSpinner.fail("Snapshot push failed");
698
- snapshotError = error instanceof Error ? error.message : String(error);
699
- pushSpan.setAttribute("push.snapshot.success", false);
754
+ pushSpan.setAttribute("push.snapshot.success", snapshotSucceeded);
755
+ pushSpan.setAttribute("push.snapshot.attempts", snapshotAttempts);
756
+ if (!snapshotSucceeded && snapshotError) {
700
757
  pushSpan.setAttribute("push.snapshot.error", snapshotError);
701
758
  }
702
759
  }
760
+ const snapshotStatus = prePushDraft
761
+ ? snapshotSucceeded
762
+ ? "succeeded"
763
+ : "failed"
764
+ : undefined;
703
765
  const memoryPath = writePushMemory(projectRoot, agentCodeStr, agentIdStr, pushTasks, succeededFiles, pushFailures);
704
766
  // ─── Baseline Update ───
705
767
  // Only commit baseline if snapshot succeeded (or no draft = legacy mode).
@@ -720,7 +782,7 @@ async function _push(pushSpan, agentCode, options = {}) {
720
782
  baselineUpdated = true;
721
783
  }
722
784
  }
723
- log(_jsx(PushFinalSummary, { succeeded: succeeded, failed: failed, baselineUpdated: baselineUpdated, logDir: logRelPath, memoryPath: memoryPath }));
785
+ log(_jsx(PushFinalSummary, { succeeded: succeeded, failed: failed, baselineUpdated: baselineUpdated, logDir: logRelPath, memoryPath: memoryPath, snapshotStatus: snapshotStatus, snapshotError: snapshotError }));
724
786
  if (failed > 0) {
725
787
  log(_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(StatusLine, { kind: "warning", label: `${failed} entity change(s) failed to push to DB.` }), pushFailures.map((f) => (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { color: "red", children: [" ", icons.error, " ", f.label] }), _jsxs(Text, { color: colors.muted, children: [" Error: ", f.error] }), f.file && _jsxs(Text, { color: colors.muted, children: [" File: ", f.file] })] }, f.label))), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: colors.info, bold: true, children: "What to do next: " }), _jsxs(Text, { color: colors.muted, children: ["Fix the issues above and re-run ", _jsx(Text, { bold: true, children: "aui push" }), " to retry the failed changes."] })] })] }));
726
788
  }
@@ -736,17 +798,77 @@ async function _push(pushSpan, agentCode, options = {}) {
736
798
  log(_jsx(StatusLine, { kind: "warning", label: `Snapshot saved for draft ${prePushDraft.label}, but ${failed} DB update(s) failed. Re-run "aui push" to retry the DB updates before publishing.` }));
737
799
  }
738
800
  }
801
+ const snapshotFailed = snapshotStatus === "failed";
739
802
  pushSpan.setAttribute("push.exit_reason", failed > 0
740
803
  ? succeeded > 0
741
804
  ? "partial_failure"
742
805
  : "failed"
743
- : "completed");
806
+ : snapshotFailed
807
+ ? "snapshot_failed"
808
+ : "completed");
744
809
  pushSpan.setAttribute("push.succeeded_count", succeeded);
745
810
  pushSpan.setAttribute("push.failed_count", failed);
811
+ // ─── Final structured JSON envelope (success path) ───
812
+ // The BFF / CI consumes --json output programmatically. On success we
813
+ // emit a single envelope at the very end so the caller can parse one
814
+ // top-level JSON document with the full push outcome. Failure paths
815
+ // are handled by handleError() via outputJsonError().
816
+ if (json && !snapshotFailed && failed === 0) {
817
+ outputJson({
818
+ agent: {
819
+ code: agentCodeStr,
820
+ id: agentIdStr,
821
+ },
822
+ version: prePushDraft
823
+ ? { id: prePushDraft.versionId, label: prePushDraft.label }
824
+ : undefined,
825
+ succeeded_count: succeeded,
826
+ failed_count: 0,
827
+ succeeded_files: succeededFiles,
828
+ snapshot: snapshotStatus
829
+ ? {
830
+ status: snapshotStatus,
831
+ attempts: snapshotAttempts,
832
+ }
833
+ : undefined,
834
+ baseline_updated: baselineUpdated,
835
+ log_dir: logRelPath,
836
+ memory_path: memoryPath,
837
+ });
838
+ }
839
+ // ─── Failure throws (loud + non-zero exit + JSON error envelope) ───
840
+ // Snapshot failure takes priority because if the snapshot doesn't land
841
+ // the version's file history is incomplete, even when individual DB
842
+ // updates succeeded. Falls through to handleError() which prints the
843
+ // formatted error and exits non-zero.
844
+ if (snapshotFailed) {
845
+ throw new CLIError(`Snapshot upload failed${snapshotError ? `: ${snapshotError}` : ""}`, {
846
+ suggestion: "Re-run `aui push` to retry the snapshot. Your local files are the source of truth — they remain unchanged.",
847
+ });
848
+ }
849
+ // Entity-task partial / total failure. Without this throw, `aui push`
850
+ // would print "X failed" to stdout and exit 0 — the BFF would have no
851
+ // way to know the push didn't fully apply, and Logfire would record a
852
+ // "completed" span. This was Aviram and Dor's reported bug.
853
+ if (failed > 0) {
854
+ const labels = pushFailures.map((f) => f.label).join(", ");
855
+ throw new CLIError(`${failed} entity push task(s) failed: ${labels}`, {
856
+ suggestion: succeeded > 0
857
+ ? `${succeeded} change(s) were saved. Fix the errors above and re-run \`aui push\` to retry the rest.`
858
+ : "Fix the errors above and re-run `aui push`.",
859
+ });
860
+ }
746
861
  }
747
862
  catch (error) {
748
863
  if (spinner)
749
864
  spinner.fail("Push failed");
865
+ // CLIErrors carry actionable info and a meaningful exit code — let
866
+ // handleError() format them and exit non-zero. Without this re-throw
867
+ // a snapshot-failure CLIError thrown above would be swallowed in TUI
868
+ // mode and the process would exit 0.
869
+ if (error instanceof CLIError) {
870
+ throw error;
871
+ }
750
872
  if (!json)
751
873
  log(_jsx(ErrorDisplay, { error: error }));
752
874
  else
@@ -754,6 +876,10 @@ async function _push(pushSpan, agentCode, options = {}) {
754
876
  }
755
877
  }
756
878
  async function resolveVersionDraft(config, projectConfig, session, explicitVersionId) {
879
+ // Every error path below MUST throw a typed CLIError (not return null).
880
+ // Returning null silently exits the CLI with code 0 — the BFF then thinks
881
+ // the push succeeded when nothing actually happened, and the failure
882
+ // never reaches Logfire because no exception bubbled to handleError.
757
883
  const client = new AUIClient({
758
884
  baseUrl: config.apiUrl,
759
885
  authToken: config.authToken,
@@ -800,24 +926,29 @@ async function resolveVersionDraft(config, projectConfig, session, explicitVersi
800
926
  }
801
927
  }
802
928
  if (!agentInfo) {
803
- log(_jsx(ErrorDisplay, { message: "Could not resolve agent for version management.", suggestion: "Run `aui import-agent` to link an agent, or check your session with `aui status`." }));
804
- return null;
929
+ throw new ConfigError("Could not resolve agent for version management.", {
930
+ suggestion: "Run `aui import-agent` to link an agent, or check your session with `aui status`.",
931
+ });
805
932
  }
806
933
  // If user passed --version-id, validate it's a draft
807
934
  if (explicitVersionId) {
935
+ let ver;
808
936
  try {
809
- const ver = await client.agentManagement.getVersion(agentInfo.id, explicitVersionId);
810
- if (ver.status !== "draft") {
811
- log(_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(StatusLine, { kind: "error", label: `Version v${ver.version_number} is "${ver.status}" — you can only push to a draft version.` }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.info, bold: true, children: "What to do next:" }), _jsxs(Text, { color: colors.muted, children: [" 1. Create a new draft version: ", _jsx(Text, { bold: true, children: "aui version create" })] }), _jsxs(Text, { color: colors.muted, children: [" 2. Then push with: ", _jsxs(Text, { bold: true, children: ["aui push --version-id ", '<new-draft-id>'] })] })] })] }));
812
- return null;
813
- }
814
- const label = `v${ver.version_number}`;
815
- return { versionId: ver.id, label, agentId: agentInfo.id };
937
+ ver = await client.agentManagement.getVersion(agentInfo.id, explicitVersionId);
816
938
  }
817
939
  catch (error) {
818
- log(_jsx(ErrorDisplay, { message: `Could not fetch version "${explicitVersionId}": ${error instanceof Error ? error.message : String(error)}`, suggestion: "Check the version ID with `aui version list` and try again." }));
819
- return null;
940
+ throw new CLIError(`Could not fetch version "${explicitVersionId}": ${error instanceof Error ? error.message : String(error)}`, {
941
+ suggestion: "Check the version ID with `aui version list` and try again.",
942
+ cause: error,
943
+ });
820
944
  }
945
+ if (ver.status !== "draft") {
946
+ throw new ValidationError(`Version v${ver.version_number} is "${ver.status}" — you can only push to a draft version.`, {
947
+ suggestion: "Create a new draft with `aui version create`, then push with `aui push --version-id <new-draft-id>`.",
948
+ });
949
+ }
950
+ const label = `v${ver.version_number}`;
951
+ return { versionId: ver.id, label, agentId: agentInfo.id };
821
952
  }
822
953
  // Resolve from .auirc version_id or auto-detect drafts
823
954
  let allVersions = [];
@@ -825,17 +956,20 @@ async function resolveVersionDraft(config, projectConfig, session, explicitVersi
825
956
  const versionsResp = await client.agentManagement.listVersions(agentInfo.id, 1, 50);
826
957
  allVersions = versionsResp.items;
827
958
  }
828
- catch {
829
- log(_jsx(ErrorDisplay, { message: "Could not fetch versions for this agent.", suggestion: "Check your connection and try again. Use `aui version list` to debug." }));
830
- return null;
959
+ catch (error) {
960
+ throw new CLIError("Could not fetch versions for this agent.", {
961
+ suggestion: "Check your connection and try again. Use `aui version list` to debug.",
962
+ cause: error,
963
+ });
831
964
  }
832
965
  // If .auirc has a version_id, validate it's still a draft
833
966
  if (projectConfig.version_id) {
834
967
  const configVersion = allVersions.find((v) => v.id === projectConfig.version_id);
835
968
  if (configVersion) {
836
969
  if (configVersion.status !== "draft") {
837
- log(_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(StatusLine, { kind: "error", label: `The version in your .auirc (v${configVersion.version_number}) is "${configVersion.status}" — you can only push to a draft version.` }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.info, bold: true, children: "What to do next:" }), _jsxs(Text, { color: colors.muted, children: [" 1. Create a new draft version: ", _jsx(Text, { bold: true, children: "aui version create" })] }), _jsxs(Text, { color: colors.muted, children: [" 2. Import the new draft: ", _jsxs(Text, { bold: true, children: ["aui import-agent --version ", '<new-draft-id>'] })] }), _jsxs(Text, { color: colors.muted, children: [" 3. Then push your changes: ", _jsx(Text, { bold: true, children: "aui push" })] })] })] }));
838
- return null;
970
+ throw new ValidationError(`The version in your .auirc (v${configVersion.version_number}) is "${configVersion.status}" — you can only push to a draft version.`, {
971
+ suggestion: "Create a new draft (`aui version create`), import it (`aui import-agent --version <new-draft-id>`), then `aui push`.",
972
+ });
839
973
  }
840
974
  const label = `v${configVersion.version_number}`;
841
975
  return { versionId: configVersion.id, label, agentId: agentInfo.id };
@@ -844,8 +978,9 @@ async function resolveVersionDraft(config, projectConfig, session, explicitVersi
844
978
  // Auto-detect drafts
845
979
  const drafts = allVersions.filter((v) => v.status === "draft");
846
980
  if (drafts.length === 0) {
847
- log(_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(StatusLine, { kind: "error", label: "No draft version found \u2014 you can only push to a draft version." }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.info, bold: true, children: "What to do next:" }), _jsxs(Text, { color: colors.muted, children: [" 1. Create a new draft version: ", _jsx(Text, { bold: true, children: "aui version create" })] }), _jsxs(Text, { color: colors.muted, children: [" 2. Import the draft: ", _jsxs(Text, { bold: true, children: ["aui import-agent --version ", '<draft-id>'] })] }), _jsxs(Text, { color: colors.muted, children: [" 3. Then push your changes: ", _jsx(Text, { bold: true, children: "aui push" })] })] })] }));
848
- return null;
981
+ throw new ValidationError("No draft version found you can only push to a draft version.", {
982
+ suggestion: "Create a new draft (`aui version create`), import it (`aui import-agent --version <draft-id>`), then `aui push`.",
983
+ });
849
984
  }
850
985
  if (drafts.length === 1) {
851
986
  const draft = drafts[0];
@@ -894,6 +1029,8 @@ async function pushSnapshot(client, agentId, versionId, projectRoot, fileData) {
894
1029
  }
895
1030
  // ─── Agent Settings Params Resolution ───
896
1031
  async function resolveAgentSettingsParams(config, projectConfig, session, projectRoot, scopeLevel) {
1032
+ // Throws ConfigError on any missing config field — never returns null.
1033
+ // See note on resolveVersionDraft for why silent returns are forbidden.
897
1034
  const networkId = projectConfig.agent_id || session.network_id;
898
1035
  const accountId = projectConfig.account_id || config.accountId;
899
1036
  const organizationId = projectConfig.organization_id || config.organizationId;
@@ -906,8 +1043,9 @@ async function resolveAgentSettingsParams(config, projectConfig, session, projec
906
1043
  missing.push("account_id (in .auirc or session)");
907
1044
  if (!organizationId)
908
1045
  missing.push("organization_id (in .auirc or session)");
909
- log(_jsx(ErrorDisplay, { message: `Missing: ${missing.join(", ")}`, suggestion: "Fix: re-import the agent (`aui import`) or re-login (`aui login`)." }));
910
- return null;
1046
+ throw new ConfigError(`Missing: ${missing.join(", ")}`, {
1047
+ suggestion: "Re-import the agent (`aui import`) or re-login (`aui login`).",
1048
+ });
911
1049
  }
912
1050
  let categoryId = projectConfig.network_category_id;
913
1051
  if (!categoryId) {
@@ -931,8 +1069,9 @@ async function resolveAgentSettingsParams(config, projectConfig, session, projec
931
1069
  }
932
1070
  }
933
1071
  if (!categoryId) {
934
- log(_jsx(ErrorDisplay, { message: "Missing network_category_id.", suggestion: "Re-import the agent to fix." }));
935
- return null;
1072
+ throw new ConfigError("Missing network_category_id.", {
1073
+ suggestion: "Re-import the agent (`aui import`) to fix.",
1074
+ });
936
1075
  }
937
1076
  const baseParams = {
938
1077
  updated_by: userId,