@pushpalsdev/cli 1.0.5 → 1.0.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.
Files changed (73) hide show
  1. package/dist/pushpals-cli.js +788 -64
  2. package/package.json +3 -2
  3. package/runtime/.env.example +73 -0
  4. package/runtime/configs/backend.toml +79 -0
  5. package/runtime/configs/default.toml +259 -0
  6. package/runtime/configs/dev.toml +2 -0
  7. package/runtime/configs/local.example.toml +124 -0
  8. package/runtime/prompts/localbuddy/local_quick_reply_json_system_suffix.md +2 -0
  9. package/runtime/prompts/localbuddy/local_quick_reply_system_prompt.md +10 -0
  10. package/runtime/prompts/localbuddy/local_quick_reply_user_prompt.md +2 -0
  11. package/runtime/prompts/localbuddy/localbuddy_planner_git_diff_section.md +3 -0
  12. package/runtime/prompts/localbuddy/localbuddy_planner_git_status_section.md +3 -0
  13. package/runtime/prompts/localbuddy/localbuddy_planner_output_contract.md +5 -0
  14. package/runtime/prompts/localbuddy/localbuddy_planner_user_prompt.md +2 -0
  15. package/runtime/prompts/localbuddy/localbuddy_system_prompt.md +110 -0
  16. package/runtime/prompts/remotebuddy/autonomy_ideation_system_prompt.md +60 -0
  17. package/runtime/prompts/remotebuddy/autonomy_planning_system_prompt.md +5 -0
  18. package/runtime/prompts/remotebuddy/autonomy_scoring_system_prompt.md +6 -0
  19. package/runtime/prompts/remotebuddy/codex_adapter_json_requirements.md +1 -0
  20. package/runtime/prompts/remotebuddy/codex_adapter_json_schema_intro.md +1 -0
  21. package/runtime/prompts/remotebuddy/codex_adapter_max_tokens_line.md +1 -0
  22. package/runtime/prompts/remotebuddy/codex_adapter_prompt_template.md +14 -0
  23. package/runtime/prompts/remotebuddy/context_packer_condensed_history_system_prompt.md +1 -0
  24. package/runtime/prompts/remotebuddy/context_packer_system_prompt.md +1 -0
  25. package/runtime/prompts/remotebuddy/context_packer_user_prompt.md +11 -0
  26. package/runtime/prompts/remotebuddy/fallback_file_system_prompt.md +1 -0
  27. package/runtime/prompts/remotebuddy/fallback_file_user_prompt.md +4 -0
  28. package/runtime/prompts/remotebuddy/planner_post_system_prompt.md +2 -0
  29. package/runtime/prompts/remotebuddy/planner_repair_suffix_prompt.md +1 -0
  30. package/runtime/prompts/remotebuddy/planner_repair_user_prompt.md +7 -0
  31. package/runtime/prompts/remotebuddy/remotebuddy_system_prompt.md +109 -0
  32. package/runtime/prompts/review_agent/fix_job_intro_line.md +1 -0
  33. package/runtime/prompts/review_agent/merge_conflict_context_intro_line.md +1 -0
  34. package/runtime/prompts/review_agent/merge_conflict_instruction.md +4 -0
  35. package/runtime/prompts/review_agent/review_prompt_template.md +18 -0
  36. package/runtime/prompts/review_agent/reviewer.md +39 -0
  37. package/runtime/prompts/shared/post_system_prompt.md +62 -0
  38. package/runtime/prompts/workerpals/codex_quality_critic_instruction_prompt.md +14 -0
  39. package/runtime/prompts/workerpals/commit_message_prompt.md +36 -0
  40. package/runtime/prompts/workerpals/commit_message_user_prompt.md +7 -0
  41. package/runtime/prompts/workerpals/miniswe_broker_system_prompt.md +33 -0
  42. package/runtime/prompts/workerpals/miniswe_broker_task_prompt.md +5 -0
  43. package/runtime/prompts/workerpals/miniswe_completion_requirement.md +1 -0
  44. package/runtime/prompts/workerpals/miniswe_context_compaction_retry_prompt.md +1 -0
  45. package/runtime/prompts/workerpals/miniswe_explicit_targets_block.md +2 -0
  46. package/runtime/prompts/workerpals/miniswe_recovery_guidance_base.md +4 -0
  47. package/runtime/prompts/workerpals/miniswe_recovery_guidance_blocker_line.md +1 -0
  48. package/runtime/prompts/workerpals/miniswe_strict_tool_use_guidance.md +6 -0
  49. package/runtime/prompts/workerpals/miniswe_supplemental_guidance_section.md +2 -0
  50. package/runtime/prompts/workerpals/miniswe_timeout_note.md +1 -0
  51. package/runtime/prompts/workerpals/miniswe_toolcall_retry_guidance.md +1 -0
  52. package/runtime/prompts/workerpals/openai_codex_default_system_prompt.md +4 -0
  53. package/runtime/prompts/workerpals/openai_codex_instruction_wrapper.md +5 -0
  54. package/runtime/prompts/workerpals/openai_codex_runtime_policy_appendix.md +5 -0
  55. package/runtime/prompts/workerpals/openai_codex_supplemental_guidance_section.md +2 -0
  56. package/runtime/prompts/workerpals/openai_codex_task_execute_system_prompt.md +12 -0
  57. package/runtime/prompts/workerpals/openhands_minimal_security_policy.j2 +8 -0
  58. package/runtime/prompts/workerpals/openhands_minimal_system_prompt.j2 +20 -0
  59. package/runtime/prompts/workerpals/openhands_strict_tool_use_message.md +1 -0
  60. package/runtime/prompts/workerpals/openhands_supplemental_guidance_message.md +2 -0
  61. package/runtime/prompts/workerpals/openhands_task_execute_fallback_system_prompt.md +1 -0
  62. package/runtime/prompts/workerpals/openhands_task_execute_system_prompt.md +21 -0
  63. package/runtime/prompts/workerpals/openhands_task_user_prompt.md +6 -0
  64. package/runtime/prompts/workerpals/openhands_timeout_note.md +1 -0
  65. package/runtime/prompts/workerpals/pr_description.md +42 -0
  66. package/runtime/prompts/workerpals/task_quality_critic_system_prompt.md +9 -0
  67. package/runtime/prompts/workerpals/task_quality_critic_user_prompt.md +17 -0
  68. package/runtime/prompts/workerpals/workerpals_system_prompt.md +115 -0
  69. package/runtime/protocol/schemas/approvals.schema.json +6 -0
  70. package/runtime/protocol/schemas/envelope.schema.json +96 -0
  71. package/runtime/protocol/schemas/events.schema.json +679 -0
  72. package/runtime/protocol/schemas/http.schema.json +50 -0
  73. package/runtime/vision.example.md +191 -0
@@ -2,10 +2,14 @@
2
2
  // @bun
3
3
 
4
4
  // ../../scripts/pushpals-cli.ts
5
- import { appendFileSync, chmodSync, cpSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
6
- import { dirname, join as join2, resolve as resolve2 } from "path";
5
+ import { appendFileSync, chmodSync, cpSync, existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
6
+ import { dirname, join as join2, resolve as resolve3 } from "path";
7
7
  import { createInterface } from "readline";
8
8
 
9
+ // ../shared/src/client_preflight.ts
10
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
11
+ import { relative, resolve as resolve2 } from "path";
12
+
9
13
  // ../shared/src/config.ts
10
14
  import { existsSync, readFileSync } from "fs";
11
15
  import { join, resolve, isAbsolute } from "path";
@@ -739,15 +743,327 @@ function loadPushPalsConfig(options = {}) {
739
743
  return config;
740
744
  }
741
745
 
746
+ // ../shared/src/vision.ts
747
+ var SECTION_HEADING_RE = /^##\s+(\d+)\)\s+(.+?)\s*$/;
748
+ var ONE_SENTENCE_PROMPT_RE = /^\>\s*\*\*One sentence:\*\*\s*(.+)\s*$/i;
749
+ var BLOCKQUOTE_RE = /^\>\s*(.+?)\s*$/;
750
+ function toLines(markdown) {
751
+ return String(markdown ?? "").replace(/\r\n/g, `
752
+ `).split(`
753
+ `);
754
+ }
755
+ function extractOneSentence(lines) {
756
+ let expectNextBlockquoteSentence = false;
757
+ for (const line of lines) {
758
+ const marker = line.match(ONE_SENTENCE_PROMPT_RE);
759
+ if (marker) {
760
+ const inline = marker[1].trim();
761
+ if (inline)
762
+ return inline;
763
+ expectNextBlockquoteSentence = true;
764
+ continue;
765
+ }
766
+ const block = line.match(BLOCKQUOTE_RE);
767
+ if (expectNextBlockquoteSentence) {
768
+ if (!block)
769
+ continue;
770
+ const text = block[1].trim();
771
+ if (!text)
772
+ continue;
773
+ if (/^Example:/i.test(text))
774
+ continue;
775
+ return text;
776
+ }
777
+ }
778
+ for (const line of lines) {
779
+ const block = line.match(BLOCKQUOTE_RE);
780
+ if (!block)
781
+ continue;
782
+ const text = block[1].trim();
783
+ if (!text)
784
+ continue;
785
+ if (/^\*\*One sentence:\*\*/i.test(text))
786
+ continue;
787
+ if (/^Example:/i.test(text))
788
+ continue;
789
+ return text;
790
+ }
791
+ return "";
792
+ }
793
+ function parseVisionDoc(markdown) {
794
+ const lines = toLines(markdown);
795
+ const sections = [];
796
+ let currentNumber = "";
797
+ let currentTitle = "";
798
+ let currentBody = [];
799
+ const flushCurrent = () => {
800
+ if (!currentNumber)
801
+ return;
802
+ sections.push({
803
+ number: currentNumber,
804
+ title: currentTitle,
805
+ markdown: currentBody.join(`
806
+ `).trim()
807
+ });
808
+ currentNumber = "";
809
+ currentTitle = "";
810
+ currentBody = [];
811
+ };
812
+ for (const line of lines) {
813
+ const heading = line.match(SECTION_HEADING_RE);
814
+ if (heading) {
815
+ flushCurrent();
816
+ currentNumber = heading[1];
817
+ currentTitle = heading[2].trim();
818
+ continue;
819
+ }
820
+ if (currentNumber) {
821
+ currentBody.push(line);
822
+ }
823
+ }
824
+ flushCurrent();
825
+ const sectionByNumber = {};
826
+ for (const section of sections) {
827
+ if (!sectionByNumber[section.number]) {
828
+ sectionByNumber[section.number] = section;
829
+ }
830
+ }
831
+ return {
832
+ oneSentence: extractOneSentence(lines),
833
+ sections,
834
+ sectionByNumber
835
+ };
836
+ }
837
+ function validateVisionDocStructure(markdown) {
838
+ const parsed = parseVisionDoc(markdown);
839
+ const missingSectionNumbers = [];
840
+ const errors = [];
841
+ if (!parsed.oneSentence) {
842
+ errors.push('Missing one-sentence vision line (expected near the top as a blockquote after "**One sentence:**").');
843
+ }
844
+ return {
845
+ ok: errors.length === 0,
846
+ sectionCount: parsed.sections.length,
847
+ hasOneSentence: Boolean(parsed.oneSentence),
848
+ missingSectionNumbers,
849
+ errors
850
+ };
851
+ }
852
+
853
+ // ../shared/src/client_preflight.ts
854
+ function runtimeHasConfigDir(runtimeRoot, dirName) {
855
+ const dirPath = resolve2(runtimeRoot, dirName);
856
+ return existsSync2(resolve2(dirPath, "default.toml")) || existsSync2(resolve2(dirPath, "local.example.toml")) || existsSync2(resolve2(dirPath, "local.toml"));
857
+ }
858
+ function resolveClientConfigDir(projectRoot, runtimeRoot, explicitConfigDir) {
859
+ if (explicitConfigDir && explicitConfigDir.trim()) {
860
+ return resolve2(explicitConfigDir);
861
+ }
862
+ const runtimeCanonical = resolve2(runtimeRoot, "configs");
863
+ if (runtimeHasConfigDir(runtimeRoot, "configs")) {
864
+ return runtimeCanonical;
865
+ }
866
+ const runtimeLegacy = resolve2(runtimeRoot, "config");
867
+ if (runtimeHasConfigDir(runtimeRoot, "config")) {
868
+ return runtimeLegacy;
869
+ }
870
+ const projectCanonical = resolve2(projectRoot, "configs");
871
+ if (runtimeHasConfigDir(projectRoot, "configs")) {
872
+ return projectCanonical;
873
+ }
874
+ const projectLegacy = resolve2(projectRoot, "config");
875
+ if (runtimeHasConfigDir(projectRoot, "config")) {
876
+ return projectLegacy;
877
+ }
878
+ return runtimeCanonical;
879
+ }
880
+ function toDisplayPath(currentRoot, pathValue) {
881
+ const rel = relative(currentRoot, pathValue);
882
+ if (!rel || rel === "")
883
+ return ".";
884
+ if (rel.startsWith(".."))
885
+ return pathValue;
886
+ return rel.replace(/\\/g, "/");
887
+ }
888
+ function quotePowerShell(pathValue) {
889
+ if (/^[A-Za-z0-9_./\\:-]+$/.test(pathValue))
890
+ return pathValue;
891
+ return `'${pathValue.replace(/'/g, "''")}'`;
892
+ }
893
+ function quoteBash(pathValue) {
894
+ if (/^[A-Za-z0-9_./\\:-]+$/.test(pathValue))
895
+ return pathValue;
896
+ return "'" + pathValue.replace(/'/g, `'"'"'`) + "'";
897
+ }
898
+ function buildCopyCommands(workspaceRoot, sourcePath, destPath) {
899
+ const displaySource = toDisplayPath(workspaceRoot, sourcePath);
900
+ const displayDest = toDisplayPath(workspaceRoot, destPath);
901
+ return {
902
+ windowsPowerShell: `Copy-Item ${quotePowerShell(displaySource)} ${quotePowerShell(displayDest)}`,
903
+ bash: `cp ${quoteBash(displaySource)} ${quoteBash(displayDest)}`
904
+ };
905
+ }
906
+ function evaluateClientRuntimePreflight(options) {
907
+ const projectRoot = resolve2(options.projectRoot);
908
+ const runtimeRoot = resolve2(options.runtimeRoot ?? projectRoot);
909
+ const configDir = resolveClientConfigDir(projectRoot, runtimeRoot, options.configDir);
910
+ const visionTemplateRoot = resolve2(options.visionTemplateRoot ?? runtimeRoot);
911
+ const config = options.config ?? loadPushPalsConfig({
912
+ projectRoot,
913
+ configDir,
914
+ reload: true
915
+ });
916
+ const issues = [];
917
+ const envPath = resolve2(runtimeRoot, ".env");
918
+ if (!existsSync2(envPath)) {
919
+ const envExamplePath = resolve2(runtimeRoot, ".env.example");
920
+ issues.push({
921
+ code: "missing_env_file",
922
+ message: `Missing required local env file: ${toDisplayPath(projectRoot, envPath)}.`,
923
+ copyCommands: existsSync2(envExamplePath) ? buildCopyCommands(projectRoot, envExamplePath, envPath) : undefined
924
+ });
925
+ }
926
+ const localTomlPath = resolve2(runtimeRoot, "configs", "local.toml");
927
+ const legacyLocalTomlPath = resolve2(runtimeRoot, "config", "local.toml");
928
+ if (!existsSync2(localTomlPath) && !existsSync2(legacyLocalTomlPath)) {
929
+ const localExamplePath = resolve2(runtimeRoot, "configs", "local.example.toml");
930
+ issues.push({
931
+ code: "missing_local_toml",
932
+ message: `Missing required local config file: ${toDisplayPath(projectRoot, localTomlPath)}.`,
933
+ copyCommands: existsSync2(localExamplePath) ? buildCopyCommands(projectRoot, localExamplePath, localTomlPath) : undefined
934
+ });
935
+ }
936
+ const autonomyEnabled = Boolean(config.remotebuddy.autonomy.enabled);
937
+ if (!autonomyEnabled) {
938
+ return {
939
+ ok: issues.length === 0,
940
+ projectRoot,
941
+ runtimeRoot,
942
+ config,
943
+ issues,
944
+ autonomyEnabled,
945
+ visionSummary: null
946
+ };
947
+ }
948
+ const visionPath = resolve2(projectRoot, "vision.md");
949
+ const visionTemplatePath = resolve2(visionTemplateRoot, "vision.example.md");
950
+ if (!existsSync2(visionPath)) {
951
+ issues.push({
952
+ code: "missing_vision_doc",
953
+ message: "Missing required autonomy vision file: vision.md " + "(required when remotebuddy.autonomy.enabled=true).",
954
+ copyCommands: existsSync2(visionTemplatePath) ? buildCopyCommands(projectRoot, visionTemplatePath, visionPath) : undefined
955
+ });
956
+ return {
957
+ ok: false,
958
+ projectRoot,
959
+ runtimeRoot,
960
+ config,
961
+ issues,
962
+ autonomyEnabled,
963
+ visionSummary: null
964
+ };
965
+ }
966
+ let rawVision = "";
967
+ try {
968
+ rawVision = readFileSync2(visionPath, "utf8");
969
+ } catch (err) {
970
+ issues.push({
971
+ code: "unreadable_vision_doc",
972
+ message: `Autonomy vision preflight failed: could not read vision.md.`,
973
+ detail: String(err)
974
+ });
975
+ return {
976
+ ok: false,
977
+ projectRoot,
978
+ runtimeRoot,
979
+ config,
980
+ issues,
981
+ autonomyEnabled,
982
+ visionSummary: null
983
+ };
984
+ }
985
+ const visionText = rawVision.trim();
986
+ if (!visionText) {
987
+ issues.push({
988
+ code: "empty_vision_doc",
989
+ message: "Autonomy vision preflight failed: vision.md is empty.",
990
+ detail: "Add repository vision/goals before startup."
991
+ });
992
+ return {
993
+ ok: false,
994
+ projectRoot,
995
+ runtimeRoot,
996
+ config,
997
+ issues,
998
+ autonomyEnabled,
999
+ visionSummary: null
1000
+ };
1001
+ }
1002
+ const validation = validateVisionDocStructure(visionText);
1003
+ if (!validation.ok) {
1004
+ issues.push({
1005
+ code: "invalid_vision_doc",
1006
+ message: "Autonomy vision preflight failed: vision.md is invalid.",
1007
+ detail: validation.errors.join(" ")
1008
+ });
1009
+ return {
1010
+ ok: false,
1011
+ projectRoot,
1012
+ runtimeRoot,
1013
+ config,
1014
+ issues,
1015
+ autonomyEnabled,
1016
+ visionSummary: null
1017
+ };
1018
+ }
1019
+ return {
1020
+ ok: issues.length === 0,
1021
+ projectRoot,
1022
+ runtimeRoot,
1023
+ config,
1024
+ issues,
1025
+ autonomyEnabled,
1026
+ visionSummary: {
1027
+ path: toDisplayPath(projectRoot, visionPath),
1028
+ chars: visionText.length,
1029
+ sectionCount: validation.sectionCount,
1030
+ validation
1031
+ }
1032
+ };
1033
+ }
1034
+ function formatClientRuntimePreflightLines(result, prefix) {
1035
+ const normalizedPrefix = prefix.trim();
1036
+ const lines = [];
1037
+ if (result.ok) {
1038
+ if (result.visionSummary) {
1039
+ lines.push(`${normalizedPrefix} Autonomy preflight: loaded ${result.visionSummary.path} ` + `(${result.visionSummary.chars} chars, ${result.visionSummary.sectionCount} section(s)).`);
1040
+ }
1041
+ return lines;
1042
+ }
1043
+ for (const issue of result.issues) {
1044
+ lines.push(`${normalizedPrefix} ${issue.message}`);
1045
+ if (issue.detail) {
1046
+ lines.push(`${normalizedPrefix} ${issue.detail}`);
1047
+ }
1048
+ if (issue.copyCommands) {
1049
+ lines.push(`${normalizedPrefix} Windows (PowerShell): ${issue.copyCommands.windowsPowerShell}`);
1050
+ lines.push(`${normalizedPrefix} Linux/macOS (bash): ${issue.copyCommands.bash}`);
1051
+ }
1052
+ }
1053
+ return lines;
1054
+ }
1055
+
742
1056
  // ../../scripts/pushpals-cli.ts
743
1057
  var DEFAULT_MONITOR_PORT = 8081;
744
1058
  var MONITOR_SCAN_PORTS = 32;
1059
+ var MONITOR_POLL_MS = 2000;
745
1060
  var HTTP_TIMEOUT_MS = 2500;
746
1061
  var LOCALBUDDY_TIMEOUT_MS = 4000;
747
1062
  var SSE_RECONNECT_MS = 1500;
748
1063
  var DEFAULT_RUNTIME_BOOT_TIMEOUT_MS = 90000;
749
1064
  var DEFAULT_RUNTIME_BOOT_POLL_MS = 1000;
750
1065
  var DEFAULT_SERVER_BOOT_TIMEOUT_MS = 20000;
1066
+ var DEFAULT_SERVICE_STABILITY_GRACE_MS = 4000;
751
1067
  var GITHUB_OWNER = "PushPalsDev";
752
1068
  var GITHUB_REPO = "pushpals";
753
1069
  var GITHUB_API_URL = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}`;
@@ -853,11 +1169,14 @@ function parsePositiveInt(value, fallback) {
853
1169
  return parsed;
854
1170
  }
855
1171
  function normalizePath(value) {
856
- const normalized = resolve2(value).replace(/\\/g, "/").replace(/\/+$/, "");
1172
+ const normalized = resolve3(value).replace(/\\/g, "/").replace(/\/+$/, "");
857
1173
  if (process.platform === "win32")
858
1174
  return normalized.toLowerCase();
859
1175
  return normalized;
860
1176
  }
1177
+ function jsonHtmlBootstrap(value) {
1178
+ return JSON.stringify(value).replace(/</g, "\\u003c");
1179
+ }
861
1180
  async function runGit(args, cwd) {
862
1181
  const proc = Bun.spawn(["git", ...args], {
863
1182
  cwd,
@@ -878,11 +1197,39 @@ async function resolveCurrentGitRepoRoot(cwd) {
878
1197
  const root = await runGit(["rev-parse", "--show-toplevel"], cwd);
879
1198
  if (!root.ok || !root.stdout)
880
1199
  return null;
881
- return resolve2(root.stdout);
1200
+ return resolve3(root.stdout);
882
1201
  }
883
1202
  function resolveDefaultRuntimeRoot() {
884
1203
  const home = process.env.USERPROFILE || process.env.HOME || process.cwd();
885
- return resolve2(home, ".pushpals", "runtime");
1204
+ return resolve3(home, ".pushpals", "runtime");
1205
+ }
1206
+ function buildRuntimeAssetSource(root, protocolSchemasDir) {
1207
+ return {
1208
+ root,
1209
+ envExamplePath: join2(root, ".env.example"),
1210
+ visionExamplePath: join2(root, "vision.example.md"),
1211
+ configsDir: join2(root, "configs"),
1212
+ promptsDir: join2(root, "prompts"),
1213
+ protocolSchemasDir
1214
+ };
1215
+ }
1216
+ function isCompleteRuntimeAssetSource(source) {
1217
+ return existsSync3(source.envExamplePath) && existsSync3(source.visionExamplePath) && existsSync3(join2(source.configsDir, "default.toml")) && existsSync3(source.promptsDir) && existsSync3(join2(source.protocolSchemasDir, "envelope.schema.json")) && existsSync3(join2(source.protocolSchemasDir, "events.schema.json"));
1218
+ }
1219
+ function resolveBundledRuntimeAssetSource() {
1220
+ const candidates = [
1221
+ buildRuntimeAssetSource(resolve3(import.meta.dir, "..", "runtime"), resolve3(import.meta.dir, "..", "runtime", "protocol", "schemas")),
1222
+ buildRuntimeAssetSource(resolve3(import.meta.dir, ".."), resolve3(import.meta.dir, "..", "packages", "protocol", "src", "schemas")),
1223
+ buildRuntimeAssetSource(resolve3(import.meta.dir, "..", "packages", "cli", "runtime"), resolve3(import.meta.dir, "..", "packages", "cli", "runtime", "protocol", "schemas"))
1224
+ ];
1225
+ for (const candidate of candidates) {
1226
+ if (isCompleteRuntimeAssetSource(candidate))
1227
+ return candidate;
1228
+ }
1229
+ return null;
1230
+ }
1231
+ function repoLooksLikePushPalsSourceCheckout(repoRoot) {
1232
+ return existsSync3(join2(repoRoot, "configs", "default.toml")) || existsSync3(join2(repoRoot, "config", "default.toml"));
886
1233
  }
887
1234
  function parseSemverFromPackageVersion(value) {
888
1235
  const raw = String(value ?? "").trim();
@@ -932,18 +1279,56 @@ async function resolveRuntimeReleaseTag(explicitTag) {
932
1279
  }
933
1280
  }
934
1281
  function writeTextFileIfMissing(pathValue, text) {
935
- if (existsSync2(pathValue))
1282
+ if (existsSync3(pathValue))
936
1283
  return;
937
1284
  mkdirSync(dirname(pathValue), { recursive: true });
938
1285
  writeFileSync(pathValue, text, "utf8");
939
1286
  }
940
- function copyBundledRuntimeAssets(runtimeRoot) {
941
- const bundledRoot = resolve2(import.meta.dir, "..", "runtime");
942
- if (!existsSync2(bundledRoot))
1287
+ function copyRuntimeAssetBundle(source, runtimeRoot, force) {
1288
+ mkdirSync(runtimeRoot, { recursive: true });
1289
+ cpSync(source.envExamplePath, join2(runtimeRoot, ".env.example"), {
1290
+ force,
1291
+ errorOnExist: false
1292
+ });
1293
+ cpSync(source.visionExamplePath, join2(runtimeRoot, "vision.example.md"), {
1294
+ force,
1295
+ errorOnExist: false
1296
+ });
1297
+ cpSync(source.configsDir, join2(runtimeRoot, "configs"), {
1298
+ recursive: true,
1299
+ force,
1300
+ errorOnExist: false
1301
+ });
1302
+ cpSync(source.promptsDir, join2(runtimeRoot, "prompts"), {
1303
+ recursive: true,
1304
+ force,
1305
+ errorOnExist: false
1306
+ });
1307
+ cpSync(source.protocolSchemasDir, join2(runtimeRoot, "protocol", "schemas"), {
1308
+ recursive: true,
1309
+ force,
1310
+ errorOnExist: false
1311
+ });
1312
+ }
1313
+ function copyBundledRuntimeAssets(runtimeRoot, force = true) {
1314
+ const bundledSource = resolveBundledRuntimeAssetSource();
1315
+ if (!bundledSource)
943
1316
  return false;
944
- cpSync(bundledRoot, runtimeRoot, { recursive: true, force: true });
1317
+ copyRuntimeAssetBundle(bundledSource, runtimeRoot, force);
945
1318
  return true;
946
1319
  }
1320
+ function seedRuntimePreflightAssets(runtimeRoot) {
1321
+ copyBundledRuntimeAssets(runtimeRoot, false);
1322
+ writeTextFileIfMissing(join2(runtimeRoot, ".env"), `# Local PushPals runtime environment
1323
+ `);
1324
+ const localExamplePath = join2(runtimeRoot, "configs", "local.example.toml");
1325
+ if (existsSync3(localExamplePath)) {
1326
+ writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), readFileSync3(localExamplePath, "utf8"));
1327
+ } else {
1328
+ writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), `# Local PushPals runtime overrides
1329
+ `);
1330
+ }
1331
+ }
947
1332
  async function fetchTextFromUrl(url, timeoutMs = 20000) {
948
1333
  const response = await fetchWithTimeout(url, { headers: GITHUB_HEADERS }, timeoutMs);
949
1334
  if (!response.ok) {
@@ -958,7 +1343,7 @@ async function downloadRuntimeAssetsFromSourceTag(runtimeRoot, tag) {
958
1343
  throw new Error(`Failed to fetch runtime source tree for ${tag} (HTTP ${treeResponse.status})`);
959
1344
  }
960
1345
  const treePayload = await treeResponse.json();
961
- const paths = (treePayload.tree ?? []).filter((entry) => entry.type === "blob" && typeof entry.path === "string").map((entry) => String(entry.path)).filter((pathValue) => pathValue === ".env.example" || pathValue.startsWith("configs/") || pathValue.startsWith("prompts/") || pathValue.startsWith("packages/protocol/src/schemas/"));
1346
+ const paths = (treePayload.tree ?? []).filter((entry) => entry.type === "blob" && typeof entry.path === "string").map((entry) => String(entry.path)).filter((pathValue) => pathValue === ".env.example" || pathValue === "vision.example.md" || pathValue.startsWith("configs/") || pathValue.startsWith("prompts/") || pathValue.startsWith("packages/protocol/src/schemas/"));
962
1347
  if (paths.length === 0) {
963
1348
  throw new Error(`Runtime source tree for ${tag} did not include prompts/config assets`);
964
1349
  }
@@ -973,14 +1358,14 @@ async function downloadRuntimeAssetsFromSourceTag(runtimeRoot, tag) {
973
1358
  }
974
1359
  async function ensureRuntimeAssets(runtimeRoot, runtimeTag) {
975
1360
  const markerPath = join2(runtimeRoot, ".runtime-assets-tag");
976
- const currentTag = existsSync2(markerPath) ? readFileSync2(markerPath, "utf8").trim() : "";
1361
+ const currentTag = existsSync3(markerPath) ? readFileSync3(markerPath, "utf8").trim() : "";
977
1362
  const protocolSchemasDir = join2(runtimeRoot, "protocol", "schemas");
978
- const hasProtocolSchemas = existsSync2(join2(protocolSchemasDir, "envelope.schema.json")) && existsSync2(join2(protocolSchemasDir, "events.schema.json"));
979
- const hasAssets = existsSync2(join2(runtimeRoot, ".env.example")) && existsSync2(join2(runtimeRoot, "configs", "default.toml")) && existsSync2(join2(runtimeRoot, "prompts")) && hasProtocolSchemas;
1363
+ const hasProtocolSchemas = existsSync3(join2(protocolSchemasDir, "envelope.schema.json")) && existsSync3(join2(protocolSchemasDir, "events.schema.json"));
1364
+ const hasAssets = existsSync3(join2(runtimeRoot, ".env.example")) && existsSync3(join2(runtimeRoot, "vision.example.md")) && existsSync3(join2(runtimeRoot, "configs", "default.toml")) && existsSync3(join2(runtimeRoot, "prompts")) && hasProtocolSchemas;
980
1365
  if (!hasAssets || currentTag !== runtimeTag) {
981
1366
  copyBundledRuntimeAssets(runtimeRoot);
982
- const hasProtocolSchemasAfterCopy = existsSync2(join2(protocolSchemasDir, "envelope.schema.json")) && existsSync2(join2(protocolSchemasDir, "events.schema.json"));
983
- const hasAssetsAfterCopy = existsSync2(join2(runtimeRoot, ".env.example")) && existsSync2(join2(runtimeRoot, "configs", "default.toml")) && existsSync2(join2(runtimeRoot, "prompts")) && hasProtocolSchemasAfterCopy;
1367
+ const hasProtocolSchemasAfterCopy = existsSync3(join2(protocolSchemasDir, "envelope.schema.json")) && existsSync3(join2(protocolSchemasDir, "events.schema.json"));
1368
+ const hasAssetsAfterCopy = existsSync3(join2(runtimeRoot, ".env.example")) && existsSync3(join2(runtimeRoot, "vision.example.md")) && existsSync3(join2(runtimeRoot, "configs", "default.toml")) && existsSync3(join2(runtimeRoot, "prompts")) && hasProtocolSchemasAfterCopy;
984
1369
  if (!hasAssetsAfterCopy) {
985
1370
  await downloadRuntimeAssetsFromSourceTag(runtimeRoot, runtimeTag);
986
1371
  }
@@ -990,25 +1375,77 @@ async function ensureRuntimeAssets(runtimeRoot, runtimeTag) {
990
1375
  writeTextFileIfMissing(join2(runtimeRoot, ".env"), `# Local PushPals runtime environment
991
1376
  `);
992
1377
  const localExamplePath = join2(runtimeRoot, "configs", "local.example.toml");
993
- if (existsSync2(localExamplePath)) {
994
- writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), readFileSync2(localExamplePath, "utf8"));
1378
+ if (existsSync3(localExamplePath)) {
1379
+ writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), readFileSync3(localExamplePath, "utf8"));
995
1380
  } else {
996
1381
  writeTextFileIfMissing(join2(runtimeRoot, "configs", "local.toml"), `# Local PushPals runtime overrides
997
1382
  `);
998
1383
  }
999
1384
  }
1385
+ function resolveDeferredRuntimeTagHint(explicitTag) {
1386
+ return String(explicitTag || process.env.PUSHPALS_RUNTIME_TAG || "").trim();
1387
+ }
1388
+ async function prepareCliRuntime(opts) {
1389
+ const runtimeRoot = resolve3(opts.runtimeRoot || process.env.PUSHPALS_RUNTIME_ROOT || resolveDefaultRuntimeRoot());
1390
+ if (repoLooksLikePushPalsSourceCheckout(opts.repoRoot)) {
1391
+ return {
1392
+ runtimeRoot,
1393
+ runtimeTag: "",
1394
+ runtimePreflight: evaluateClientRuntimePreflight({
1395
+ projectRoot: opts.repoRoot
1396
+ }),
1397
+ preflightUsesEmbeddedRuntime: false
1398
+ };
1399
+ }
1400
+ seedRuntimePreflightAssets(runtimeRoot);
1401
+ return {
1402
+ runtimeRoot,
1403
+ runtimeTag: resolveDeferredRuntimeTagHint(opts.runtimeTag),
1404
+ runtimePreflight: evaluateClientRuntimePreflight({
1405
+ projectRoot: opts.repoRoot,
1406
+ runtimeRoot,
1407
+ visionTemplateRoot: runtimeRoot
1408
+ }),
1409
+ preflightUsesEmbeddedRuntime: true
1410
+ };
1411
+ }
1412
+ function emitCliRuntimePreflight(result) {
1413
+ const lines = formatClientRuntimePreflightLines(result, "[pushpals]");
1414
+ if (result.ok) {
1415
+ for (const line of lines)
1416
+ console.log(line);
1417
+ return;
1418
+ }
1419
+ for (const line of lines)
1420
+ console.error(line);
1421
+ }
1000
1422
  function runtimeBinaryFilename(serviceName, platformKey) {
1001
1423
  const serviceToken = serviceName === "source_control_manager" ? "source-control-manager" : serviceName;
1002
1424
  const extension = platformKey.startsWith("windows-") ? ".exe" : "";
1003
1425
  return `pushpals-runtime-${serviceToken}-${platformKey}${extension}`;
1004
1426
  }
1427
+ function buildEmbeddedRuntimeEnv(baseEnv, opts) {
1428
+ const useRuntimeConfig = opts.useRuntimeConfig !== false;
1429
+ return {
1430
+ ...baseEnv,
1431
+ PUSHPALS_REPO_ROOT_OVERRIDE: opts.repoRoot,
1432
+ PUSHPALS_PROJECT_ROOT_OVERRIDE: opts.repoRoot,
1433
+ ...useRuntimeConfig ? {
1434
+ PUSHPALS_CONFIG_DIR_OVERRIDE: join2(opts.runtimeRoot, "configs"),
1435
+ PUSHPALS_PROMPTS_ROOT_OVERRIDE: opts.runtimeRoot
1436
+ } : {
1437
+ PUSHPALS_PROMPTS_ROOT_OVERRIDE: opts.repoRoot
1438
+ },
1439
+ PUSHPALS_PROTOCOL_SCHEMAS_DIR: join2(opts.runtimeRoot, "protocol", "schemas")
1440
+ };
1441
+ }
1005
1442
  function timestampFileToken() {
1006
1443
  return new Date().toISOString().replace(/[:.]/g, "-");
1007
1444
  }
1008
1445
  function readLogTail(logPath, maxLines = 40) {
1009
- if (!existsSync2(logPath))
1446
+ if (!existsSync3(logPath))
1010
1447
  return "";
1011
- const raw = readFileSync2(logPath, "utf8");
1448
+ const raw = readFileSync3(logPath, "utf8");
1012
1449
  const lines = raw.split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.length > 0);
1013
1450
  if (lines.length === 0)
1014
1451
  return "";
@@ -1042,7 +1479,7 @@ async function ensureRuntimeBinaries(runtimeRoot, runtimeTag) {
1042
1479
  runtimeBinaries.sourceControlManager
1043
1480
  ];
1044
1481
  for (const binaryPath of requiredAssets) {
1045
- if (existsSync2(binaryPath))
1482
+ if (existsSync3(binaryPath))
1046
1483
  continue;
1047
1484
  const assetName = binaryPath.split(/[\\/]/).pop() || "";
1048
1485
  await downloadBinaryAsset(runtimeTag, assetName, binaryPath);
@@ -1112,10 +1549,25 @@ function spawnRuntimeService(name, command, cwd, env, logPath) {
1112
1549
  function stopRuntimeServices(services) {
1113
1550
  for (const service of services) {
1114
1551
  try {
1115
- service.proc.kill();
1552
+ if (process.platform === "win32" && typeof service.proc.pid === "number" && service.proc.pid > 0) {
1553
+ Bun.spawnSync(["taskkill", "/PID", String(service.proc.pid), "/T", "/F"], {
1554
+ stdin: "ignore",
1555
+ stdout: "ignore",
1556
+ stderr: "ignore"
1557
+ });
1558
+ } else {
1559
+ service.proc.kill();
1560
+ }
1116
1561
  } catch {}
1117
1562
  }
1118
1563
  }
1564
+ async function repoHasRemote(repoRoot, remote) {
1565
+ const normalizedRemote = remote.trim();
1566
+ if (!normalizedRemote)
1567
+ return false;
1568
+ const result = await runGit(["remote", "get-url", normalizedRemote], repoRoot);
1569
+ return result.ok && Boolean(result.stdout);
1570
+ }
1119
1571
  async function probeServer(serverUrl) {
1120
1572
  try {
1121
1573
  const response = await fetchWithTimeout(`${serverUrl}/healthz`, {}, HTTP_TIMEOUT_MS);
@@ -1162,21 +1614,22 @@ async function probeLocalBuddy(localAgentUrl, authToken) {
1162
1614
  return await fetchJsonWithTimeout(`${localAgentUrl}/healthz`, { headers: authHeaders(authToken) }, LOCALBUDDY_TIMEOUT_MS);
1163
1615
  }
1164
1616
  async function autoStartRuntimeServices(opts) {
1165
- const runtimeRoot = resolve2(opts.runtimeRoot || process.env.PUSHPALS_RUNTIME_ROOT || resolveDefaultRuntimeRoot());
1166
- const runtimeTag = await resolveRuntimeReleaseTag(opts.runtimeTag);
1167
- const runtimeBinaries = await ensureRuntimeBinaries(runtimeRoot, runtimeTag);
1168
- await ensureRuntimeAssets(runtimeRoot, runtimeTag);
1617
+ const { runtimePreflight } = opts.preparedRuntime;
1618
+ const runtimeRoot = opts.preparedRuntime.runtimeRoot;
1619
+ const runtimeTag = opts.preparedRuntime.runtimeTag || await resolveRuntimeReleaseTag(opts.requestedRuntimeTag);
1169
1620
  console.log(`[pushpals] LocalBuddy unavailable. Auto-starting runtime for repo: ${opts.repoRoot}`);
1170
1621
  console.log(`[pushpals] runtimeRoot=${runtimeRoot}`);
1171
1622
  console.log(`[pushpals] runtimeTag=${runtimeTag}`);
1172
- const runtimeEnv = {
1173
- ...process.env,
1174
- PUSHPALS_REPO_ROOT_OVERRIDE: opts.repoRoot,
1175
- PUSHPALS_PROJECT_ROOT_OVERRIDE: opts.repoRoot,
1176
- PUSHPALS_CONFIG_DIR_OVERRIDE: join2(runtimeRoot, "configs"),
1177
- PUSHPALS_PROMPTS_ROOT_OVERRIDE: runtimeRoot,
1178
- PUSHPALS_PROTOCOL_SCHEMAS_DIR: join2(runtimeRoot, "protocol", "schemas")
1179
- };
1623
+ if (!runtimePreflight.ok) {
1624
+ throw new Error("Embedded runtime preflight failed.");
1625
+ }
1626
+ await ensureRuntimeAssets(runtimeRoot, runtimeTag);
1627
+ const runtimeBinaries = await ensureRuntimeBinaries(runtimeRoot, runtimeTag);
1628
+ const runtimeEnv = buildEmbeddedRuntimeEnv(process.env, {
1629
+ repoRoot: opts.repoRoot,
1630
+ runtimeRoot,
1631
+ useRuntimeConfig: opts.preparedRuntime.preflightUsesEmbeddedRuntime
1632
+ });
1180
1633
  const services = [];
1181
1634
  const runToken = timestampFileToken();
1182
1635
  const logDir = join2(runtimeRoot, "logs", "bootstrap");
@@ -1224,11 +1677,14 @@ ${tail}` : ""}`);
1224
1677
  services.push(remotebuddyService);
1225
1678
  console.log(`[pushpals] remotebuddy log: ${remotebuddyService.logPath}`);
1226
1679
  const scmHealthy = await probeSourceControlManager(opts.sourceControlManagerPort);
1227
- if (!scmHealthy) {
1680
+ const scmRemoteAvailable = await repoHasRemote(opts.repoRoot, opts.sourceControlManagerRemote);
1681
+ if (!scmHealthy && scmRemoteAvailable) {
1228
1682
  console.log("[pushpals] Starting embedded SourceControlManager...");
1229
1683
  const sourceControlManagerService = spawnRuntimeService("source_control_manager", [runtimeBinaries.sourceControlManager, "--skip-clean-check"], opts.repoRoot, runtimeEnv, logPathFor("source_control_manager"));
1230
1684
  services.push(sourceControlManagerService);
1231
1685
  console.log(`[pushpals] source_control_manager log: ${sourceControlManagerService.logPath}`);
1686
+ } else if (!scmRemoteAvailable) {
1687
+ console.log(`[pushpals] Repo has no git remote "${opts.sourceControlManagerRemote}"; skipping embedded SourceControlManager.`);
1232
1688
  } else {
1233
1689
  console.log("[pushpals] SourceControlManager already healthy; skipping embedded start.");
1234
1690
  }
@@ -1256,6 +1712,19 @@ ${tail}` : ""}`);
1256
1712
  }
1257
1713
  const health = await probeLocalBuddy(opts.localAgentUrl, opts.authToken);
1258
1714
  if (health?.ok) {
1715
+ const stabilityDeadline = Date.now() + DEFAULT_SERVICE_STABILITY_GRACE_MS;
1716
+ while (Date.now() < stabilityDeadline) {
1717
+ for (const service of services) {
1718
+ if (!service.exited)
1719
+ continue;
1720
+ const tail = readLogTail(service.logPath);
1721
+ stopRuntimeServices(services);
1722
+ throw new Error(`Embedded ${service.name} exited immediately after bootstrap (code=${service.exitCode ?? "unknown"}). ` + `See ${service.logPath}${tail ? `
1723
+ --- ${service.name} log tail ---
1724
+ ${tail}` : ""}`);
1725
+ }
1726
+ await Bun.sleep(250);
1727
+ }
1259
1728
  console.log("[pushpals] Embedded runtime is ready.");
1260
1729
  return services;
1261
1730
  }
@@ -1265,10 +1734,10 @@ ${tail}` : ""}`);
1265
1734
  throw new Error(`Timed out waiting for LocalBuddy at ${opts.localAgentUrl} after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms`);
1266
1735
  }
1267
1736
  function readCliState(pathValue) {
1268
- if (!existsSync2(pathValue))
1737
+ if (!existsSync3(pathValue))
1269
1738
  return {};
1270
1739
  try {
1271
- const raw = readFileSync2(pathValue, "utf8");
1740
+ const raw = readFileSync3(pathValue, "utf8");
1272
1741
  const parsed = JSON.parse(raw);
1273
1742
  if (!parsed || typeof parsed !== "object")
1274
1743
  return {};
@@ -1309,17 +1778,221 @@ async function looksLikeMonitoringHub(url) {
1309
1778
  return false;
1310
1779
  }
1311
1780
  }
1312
- async function resolveMonitoringHubUrl(preferredUrl, fallbackPort) {
1313
- const explicit = normalizeUrl(preferredUrl);
1314
- if (explicit)
1315
- return explicit;
1316
- const basePort = fallbackPort;
1317
- for (let port = basePort;port < basePort + MONITOR_SCAN_PORTS; port++) {
1318
- const candidate = `http://localhost:${port}`;
1319
- if (await looksLikeMonitoringHub(candidate))
1320
- return candidate;
1781
+ function buildEmbeddedMonitoringHubHtml(opts) {
1782
+ const bootstrap = jsonHtmlBootstrap(opts);
1783
+ return `<!doctype html>
1784
+ <html lang="en">
1785
+ <head>
1786
+ <meta charset="utf-8" />
1787
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1788
+ <title>PushPals CLI Monitor</title>
1789
+ <style>
1790
+ :root { color-scheme: dark; --bg:#08111b; --panel:#112235; --panel2:#16324a; --line:#2b5876; --fg:#edf6ff; --muted:#90b5d6; --accent:#58d8c3; --warn:#ffbf5f; --bad:#ff7f7f; }
1791
+ * { box-sizing:border-box; }
1792
+ body { margin:0; font-family:Consolas, "SFMono-Regular", monospace; background:radial-gradient(circle at top, #0d2233, var(--bg) 56%); color:var(--fg); }
1793
+ main { max-width:1200px; margin:0 auto; padding:24px; }
1794
+ h1,h2 { margin:0 0 12px; }
1795
+ p { color:var(--muted); }
1796
+ .row { display:grid; gap:16px; margin-top:16px; }
1797
+ .cards { grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); }
1798
+ .panels { grid-template-columns:repeat(auto-fit,minmax(320px,1fr)); }
1799
+ .card, .panel { border:1px solid var(--line); background:linear-gradient(180deg,var(--panel),var(--panel2)); border-radius:16px; padding:16px; box-shadow:0 12px 40px rgba(0,0,0,.22); }
1800
+ .label { font-size:12px; letter-spacing:.12em; text-transform:uppercase; color:var(--muted); margin-bottom:10px; }
1801
+ .value { font-size:32px; font-weight:700; color:var(--accent); }
1802
+ .sub { margin-top:8px; color:var(--muted); white-space:pre-wrap; word-break:break-word; }
1803
+ .list { display:grid; gap:10px; margin-top:12px; }
1804
+ .item { border:1px solid rgba(88,216,195,.18); border-radius:12px; padding:12px; background:rgba(8,17,27,.42); }
1805
+ .meta { display:flex; flex-wrap:wrap; gap:8px; margin:12px 0 0; }
1806
+ .pill { border:1px solid var(--line); border-radius:999px; padding:6px 10px; color:var(--muted); }
1807
+ a { color:var(--accent); }
1808
+ </style>
1809
+ </head>
1810
+ <body>
1811
+ <main>
1812
+ <h1>PushPals CLI Monitor</h1>
1813
+ <p>Lightweight embedded monitor for CLI-managed runtimes.</p>
1814
+ <div class="meta" id="meta"></div>
1815
+ <section class="row cards" id="cards"></section>
1816
+ <section class="row panels">
1817
+ <div class="panel">
1818
+ <h2>Requests</h2>
1819
+ <div id="requests" class="list"></div>
1820
+ </div>
1821
+ <div class="panel">
1822
+ <h2>Jobs</h2>
1823
+ <div id="jobs" class="list"></div>
1824
+ </div>
1825
+ <div class="panel">
1826
+ <h2>Completions</h2>
1827
+ <div id="completions" class="list"></div>
1828
+ </div>
1829
+ </section>
1830
+ </main>
1831
+ <script>
1832
+ const boot = ${bootstrap};
1833
+ const pollMs = ${MONITOR_POLL_MS};
1834
+ const metaEl = document.getElementById("meta");
1835
+ const cardsEl = document.getElementById("cards");
1836
+ const requestsEl = document.getElementById("requests");
1837
+ const jobsEl = document.getElementById("jobs");
1838
+ const completionsEl = document.getElementById("completions");
1839
+
1840
+ function esc(value) {
1841
+ return String(value ?? "")
1842
+ .replace(/&/g, "&amp;")
1843
+ .replace(/</g, "&lt;")
1844
+ .replace(/>/g, "&gt;");
1845
+ }
1846
+
1847
+ async function fetchJson(path) {
1848
+ const res = await fetch(path, { cache: "no-store" });
1849
+ if (!res.ok) throw new Error(path + " -> HTTP " + res.status);
1850
+ return await res.json();
1851
+ }
1852
+
1853
+ function setList(target, rows, emptyLabel, formatter) {
1854
+ if (!Array.isArray(rows) || rows.length === 0) {
1855
+ target.innerHTML = '<div class="item">' + esc(emptyLabel) + "</div>";
1856
+ return;
1857
+ }
1858
+ target.innerHTML = rows.map((row) => '<div class="item">' + formatter(row) + "</div>").join("");
1859
+ }
1860
+
1861
+ function renderStatus(status) {
1862
+ const workers = status?.workers ?? {};
1863
+ const queues = status?.queues ?? {};
1864
+ const runtime = status?.runtime ?? {};
1865
+ const repo = status?.repo ?? {};
1866
+ const llmUsage = status?.llmUsage ?? {};
1867
+ const cards = [
1868
+ { label: "Server uptime", value: Math.round((Number(runtime.uptimeMs ?? 0) / 60000)) + "m", sub: runtime.startedAt ?? "unknown" },
1869
+ { label: "Workers online", value: String(workers.online ?? 0), sub: "busy " + String(workers.busy ?? 0) + " | idle " + String(workers.idle ?? 0) },
1870
+ { label: "Pending requests", value: String(queues.requests?.pending ?? 0), sub: "claimed " + String(queues.requests?.claimed ?? 0) },
1871
+ { label: "Pending jobs", value: String(queues.jobs?.pending ?? 0), sub: "claimed " + String(queues.jobs?.claimed ?? 0) },
1872
+ { label: "Completions", value: String(queues.completions?.pending ?? 0), sub: "processed " + String(queues.completions?.processed ?? 0) },
1873
+ { label: "LLM usage (24h)", value: String(llmUsage.totalTokens ?? 0), sub: "calls " + String(llmUsage.totalCalls ?? 0) }
1874
+ ];
1875
+ cardsEl.innerHTML = cards.map((card) => '<div class="card"><div class="label">' + esc(card.label) + '</div><div class="value">' + esc(card.value) + '</div><div class="sub">' + esc(card.sub) + '</div></div>').join("");
1876
+ metaEl.innerHTML = [
1877
+ '<span class="pill">server ' + esc(boot.serverUrl) + '</span>',
1878
+ '<span class="pill">localbuddy ' + esc(boot.localAgentUrl) + '</span>',
1879
+ '<span class="pill">session ' + esc(boot.sessionId) + '</span>',
1880
+ '<span class="pill">repo ' + esc(repo?.root ?? repo?.remoteUrl ?? "current repo") + '</span>'
1881
+ ].join("");
1882
+ }
1883
+
1884
+ function render() {
1885
+ Promise.all([
1886
+ fetchJson('/api/status'),
1887
+ fetchJson('/api/requests'),
1888
+ fetchJson('/api/jobs'),
1889
+ fetchJson('/api/completions')
1890
+ ]).then(([status, requests, jobs, completions]) => {
1891
+ renderStatus(status);
1892
+ setList(requestsEl, requests?.requests?.slice(0, 8), 'No requests', (row) =>
1893
+ '<strong>' + esc(row?.priority ?? 'request') + '</strong><div class="sub">' +
1894
+ esc((row?.status ?? 'unknown') + ' | ' + (row?.id ?? '')) + '</div><div class="sub">' +
1895
+ esc(String(row?.prompt ?? '').slice(0, 220)) + '</div>');
1896
+ setList(jobsEl, jobs?.jobs?.slice(0, 8), 'No jobs', (row) =>
1897
+ '<strong>' + esc(row?.kind ?? 'job') + '</strong><div class="sub">' +
1898
+ esc((row?.status ?? 'unknown') + ' | worker ' + (row?.workerId ?? '--')) + '</div><div class="sub">' +
1899
+ esc((row?.summary ?? row?.error ?? row?.id ?? '').slice(0, 220)) + '</div>');
1900
+ setList(completionsEl, completions?.completions?.slice(0, 8), 'No completions', (row) =>
1901
+ '<strong>' + esc(row?.status ?? 'completion') + '</strong><div class="sub">' +
1902
+ esc((row?.jobId ?? '') + ' | ' + (row?.commitSha ?? '')) + '</div><div class="sub">' +
1903
+ esc((row?.message ?? '').slice(0, 220)) + '</div>');
1904
+ }).catch((err) => {
1905
+ cardsEl.innerHTML = '<div class="card"><div class="label">Monitor error</div><div class="sub">' + esc(err?.message ?? err) + '</div></div>';
1906
+ });
1907
+ }
1908
+
1909
+ render();
1910
+ setInterval(render, pollMs);
1911
+ </script>
1912
+ </body>
1913
+ </html>`;
1914
+ }
1915
+ async function proxyMonitoringHubRequest(serverUrl, authToken, pathValue) {
1916
+ const target = `${serverUrl}${pathValue}`;
1917
+ const upstream = await fetchWithTimeout(target, { headers: authHeaders(authToken) }, 1e4);
1918
+ const body = await upstream.text();
1919
+ return new Response(body, {
1920
+ status: upstream.status,
1921
+ headers: {
1922
+ "content-type": String(upstream.headers.get("content-type") ?? "application/json"),
1923
+ "cache-control": "no-store"
1924
+ }
1925
+ });
1926
+ }
1927
+ async function startEmbeddedMonitoringHub(opts) {
1928
+ const html = buildEmbeddedMonitoringHubHtml({
1929
+ serverUrl: opts.serverUrl,
1930
+ localAgentUrl: opts.localAgentUrl,
1931
+ sessionId: opts.sessionId
1932
+ });
1933
+ const candidatePorts = Array.from({ length: MONITOR_SCAN_PORTS }, (_, index) => opts.preferredPort + index).concat(0);
1934
+ for (const port of candidatePorts) {
1935
+ try {
1936
+ const server = Bun.serve({
1937
+ port,
1938
+ idleTimeout: 30,
1939
+ fetch: async (req) => {
1940
+ const url = new URL(req.url);
1941
+ if (url.pathname === "/") {
1942
+ return new Response(html, {
1943
+ headers: {
1944
+ "content-type": "text/html; charset=utf-8",
1945
+ "cache-control": "no-store"
1946
+ }
1947
+ });
1948
+ }
1949
+ if (url.pathname === "/healthz") {
1950
+ return Response.json({ ok: true, port, serverUrl: opts.serverUrl, sessionId: opts.sessionId });
1951
+ }
1952
+ if (url.pathname === "/api/status") {
1953
+ return await proxyMonitoringHubRequest(opts.serverUrl, opts.authToken, "/system/status");
1954
+ }
1955
+ if (url.pathname === "/api/requests") {
1956
+ return await proxyMonitoringHubRequest(opts.serverUrl, opts.authToken, "/requests?status=all&limit=20");
1957
+ }
1958
+ if (url.pathname === "/api/jobs") {
1959
+ return await proxyMonitoringHubRequest(opts.serverUrl, opts.authToken, "/jobs?status=all&limit=20");
1960
+ }
1961
+ if (url.pathname === "/api/completions") {
1962
+ return await proxyMonitoringHubRequest(opts.serverUrl, opts.authToken, "/completions?status=all&limit=20");
1963
+ }
1964
+ return new Response("Not found", { status: 404 });
1965
+ }
1966
+ });
1967
+ return {
1968
+ url: `http://127.0.0.1:${server.port}`,
1969
+ port: Number(server.port),
1970
+ embedded: true,
1971
+ stop: () => server.stop(true)
1972
+ };
1973
+ } catch {}
1321
1974
  }
1322
- return `http://localhost:${basePort}`;
1975
+ return null;
1976
+ }
1977
+ async function resolveMonitoringHub(opts) {
1978
+ const explicit = normalizeUrl(opts.preferredUrl);
1979
+ if (explicit) {
1980
+ if (await looksLikeMonitoringHub(explicit)) {
1981
+ return { url: explicit, port: 0, stop: () => {}, embedded: false };
1982
+ }
1983
+ console.warn(`[pushpals] Preferred monitoring hub ${explicit} is unavailable; starting embedded monitor instead.`);
1984
+ }
1985
+ for (let port = opts.fallbackPort;port < opts.fallbackPort + MONITOR_SCAN_PORTS; port++) {
1986
+ const candidate = `http://127.0.0.1:${port}`;
1987
+ if (await looksLikeMonitoringHub(candidate)) {
1988
+ return { url: candidate, port, stop: () => {}, embedded: false };
1989
+ }
1990
+ }
1991
+ const embedded = await startEmbeddedMonitoringHub(opts);
1992
+ if (!embedded) {
1993
+ console.warn("[pushpals] Embedded monitoring hub could not start on any expected local port.");
1994
+ }
1995
+ return embedded;
1323
1996
  }
1324
1997
  async function sendMessageToLocalBuddy(localAgentUrl, text) {
1325
1998
  let response;
@@ -1493,8 +2166,7 @@ async function runSessionStream(serverUrl, sessionId, authToken, print, signal)
1493
2166
  async function openMonitoringHub(url) {
1494
2167
  let cmd = null;
1495
2168
  if (process.platform === "win32") {
1496
- const escaped = url.replace(/'/g, "''");
1497
- cmd = ["powershell", "-NoProfile", "-Command", `Start-Process '${escaped}'`];
2169
+ cmd = ["cmd", "/c", "start", "", url];
1498
2170
  } else if (process.platform === "darwin") {
1499
2171
  cmd = ["open", url];
1500
2172
  } else {
@@ -1514,7 +2186,6 @@ async function main() {
1514
2186
  const parsed = parseArgs(argv);
1515
2187
  if (!parsed)
1516
2188
  return;
1517
- const config = loadPushPalsConfig();
1518
2189
  const cwd = process.cwd();
1519
2190
  const repoRoot = await resolveCurrentGitRepoRoot(cwd);
1520
2191
  if (!repoRoot) {
@@ -1523,6 +2194,25 @@ async function main() {
1523
2194
  console.error("[pushpals] Run from a repo directory, or initialize one with `git init`.");
1524
2195
  process.exit(1);
1525
2196
  }
2197
+ const preparedRuntime = await prepareCliRuntime({
2198
+ repoRoot,
2199
+ runtimeRoot: parsed.runtimeRoot,
2200
+ runtimeTag: parsed.runtimeTag
2201
+ });
2202
+ console.log("[pushpals] Running runtime preflight...");
2203
+ console.log(`[pushpals] runtimeRoot=${preparedRuntime.runtimeRoot}`);
2204
+ if (preparedRuntime.runtimeTag) {
2205
+ console.log(`[pushpals] runtimeTag=${preparedRuntime.runtimeTag}`);
2206
+ } else if (!preparedRuntime.preflightUsesEmbeddedRuntime) {
2207
+ console.log("[pushpals] runtimeTag=(deferred; using repo config for preflight)");
2208
+ } else {
2209
+ console.log("[pushpals] runtimeTag=(deferred until embedded auto-start is needed)");
2210
+ }
2211
+ emitCliRuntimePreflight(preparedRuntime.runtimePreflight);
2212
+ if (!preparedRuntime.runtimePreflight.ok) {
2213
+ process.exit(1);
2214
+ }
2215
+ const config = preparedRuntime.runtimePreflight.config;
1526
2216
  const serverUrl = normalizeUrl(parsed.serverUrl ?? process.env.PUSHPALS_SERVER_URL, config.server.url);
1527
2217
  const localAgentUrl = normalizeUrl(parsed.localAgentUrl ?? process.env.EXPO_PUBLIC_LOCAL_AGENT_URL, config.client.localAgentUrl);
1528
2218
  const sessionId = String(parsed.sessionId ?? process.env.PUSHPALS_SESSION_ID ?? config.sessionId).trim();
@@ -1542,9 +2232,10 @@ async function main() {
1542
2232
  serverUrl,
1543
2233
  localAgentUrl,
1544
2234
  sourceControlManagerPort: config.sourceControlManager.port,
2235
+ sourceControlManagerRemote: config.sourceControlManager.remote,
1545
2236
  authToken,
1546
- runtimeRoot: parsed.runtimeRoot,
1547
- runtimeTag: parsed.runtimeTag
2237
+ preparedRuntime,
2238
+ requestedRuntimeTag: parsed.runtimeTag
1548
2239
  });
1549
2240
  health = await probeLocalBuddy(localAgentUrl, authToken);
1550
2241
  } catch (err) {
@@ -1561,7 +2252,7 @@ async function main() {
1561
2252
  }
1562
2253
  process.exit(1);
1563
2254
  }
1564
- const localBuddyRepo = health.repo ? resolve2(health.repo) : "";
2255
+ const localBuddyRepo = health.repo ? resolve3(health.repo) : "";
1565
2256
  if (!localBuddyRepo) {
1566
2257
  stopAutoStartedServices();
1567
2258
  console.error("[pushpals] LocalBuddy health response did not include repo path.");
@@ -1579,20 +2270,35 @@ async function main() {
1579
2270
  if (sessionId && sessionId !== localBuddySessionId) {
1580
2271
  console.warn(`[pushpals] Requested sessionId=${sessionId}, but LocalBuddy is currently attached to sessionId=${localBuddySessionId}.`);
1581
2272
  }
1582
- const statePath = resolve2(repoRoot, ".git", "pushpals-cli-state.json");
2273
+ const statePath = resolve3(repoRoot, ".git", "pushpals-cli-state.json");
1583
2274
  const saved = readCliState(statePath);
1584
2275
  const preferredHubUrl = normalizeUrl(parsed.monitoringHubUrl ?? process.env.PUSHPALS_MONITOR_URL ?? saved.monitoringHubUrl ?? "");
1585
2276
  const monitorPort = parsePositiveInt(process.env.PUSHPALS_CLIENT_PORT, DEFAULT_MONITOR_PORT);
1586
- const monitoringHubUrl = await resolveMonitoringHubUrl(preferredHubUrl, monitorPort);
2277
+ const monitoringHub = await resolveMonitoringHub({
2278
+ preferredUrl: preferredHubUrl,
2279
+ fallbackPort: monitorPort,
2280
+ serverUrl,
2281
+ localAgentUrl,
2282
+ sessionId: localBuddySessionId,
2283
+ authToken
2284
+ });
2285
+ const monitoringHubUrl = monitoringHub?.url ?? "";
1587
2286
  writeCliState(statePath, {
1588
- monitoringHubUrl,
2287
+ monitoringHubUrl: monitoringHubUrl || undefined,
1589
2288
  serverUrl,
1590
2289
  localAgentUrl,
1591
2290
  sessionId: localBuddySessionId,
1592
2291
  repoRoot
1593
2292
  });
1594
2293
  console.log("[pushpals] Connected.");
1595
- console.log(`monitoringHubUrl=${monitoringHubUrl}`);
2294
+ if (monitoringHubUrl) {
2295
+ console.log(`monitoringHubUrl=${monitoringHubUrl}`);
2296
+ if (monitoringHub?.embedded) {
2297
+ console.log("[pushpals] Embedded monitoring hub is running.");
2298
+ }
2299
+ } else {
2300
+ console.log("monitoringHubUrl=unavailable");
2301
+ }
1596
2302
  console.log(`serverUrl=${serverUrl}`);
1597
2303
  console.log(`localAgentUrl=${localAgentUrl}`);
1598
2304
  console.log(`sessionId=${localBuddySessionId}`);
@@ -1622,10 +2328,14 @@ ${line}
1622
2328
  streamAbort.abort();
1623
2329
  if (rl)
1624
2330
  rl.close();
2331
+ try {
2332
+ monitoringHub?.stop();
2333
+ } catch {}
1625
2334
  stopAutoStartedServices();
1626
2335
  };
1627
2336
  process.once("SIGINT", requestStop);
1628
2337
  process.once("SIGTERM", requestStop);
2338
+ process.once("exit", requestStop);
1629
2339
  rl = createInterface({
1630
2340
  input: process.stdin,
1631
2341
  output: process.stdout,
@@ -1644,20 +2354,25 @@ ${line}
1644
2354
  break;
1645
2355
  }
1646
2356
  if (text === "/hub") {
1647
- console.log(`monitoringHubUrl=${monitoringHubUrl}`);
2357
+ console.log(monitoringHubUrl ? `monitoringHubUrl=${monitoringHubUrl}` : "monitoringHubUrl=unavailable");
1648
2358
  rl.prompt();
1649
2359
  continue;
1650
2360
  }
1651
2361
  if (text === "/status") {
1652
2362
  console.log(`serverUrl=${serverUrl}`);
1653
2363
  console.log(`localAgentUrl=${localAgentUrl}`);
1654
- console.log(`sessionId=${sessionId}`);
2364
+ console.log(`sessionId=${localBuddySessionId}`);
1655
2365
  console.log(`repoRoot=${repoRoot}`);
1656
- console.log(`monitoringHubUrl=${monitoringHubUrl}`);
2366
+ console.log(monitoringHubUrl ? `monitoringHubUrl=${monitoringHubUrl}` : "monitoringHubUrl=unavailable");
1657
2367
  rl.prompt();
1658
2368
  continue;
1659
2369
  }
1660
2370
  if (text === "/open") {
2371
+ if (!monitoringHubUrl) {
2372
+ console.log("[pushpals] Monitoring hub is unavailable.");
2373
+ rl.prompt();
2374
+ continue;
2375
+ }
1661
2376
  const opened = await openMonitoringHub(monitoringHubUrl);
1662
2377
  console.log(opened ? `[pushpals] Opened ${monitoringHubUrl}` : `[pushpals] Failed to open browser. Use this link: ${monitoringHubUrl}`);
1663
2378
  rl.prompt();
@@ -1672,7 +2387,16 @@ ${line}
1672
2387
  requestStop();
1673
2388
  await Promise.race([streamTask, Bun.sleep(2000)]);
1674
2389
  }
1675
- main().catch((err) => {
1676
- console.error(`[pushpals] Fatal: ${String(err)}`);
1677
- process.exit(1);
1678
- });
2390
+ if (import.meta.main) {
2391
+ main().catch((err) => {
2392
+ console.error(`[pushpals] Fatal: ${String(err)}`);
2393
+ process.exit(1);
2394
+ });
2395
+ }
2396
+ export {
2397
+ startEmbeddedMonitoringHub,
2398
+ resolveBundledRuntimeAssetSource,
2399
+ prepareCliRuntime,
2400
+ buildEmbeddedRuntimeEnv,
2401
+ buildEmbeddedMonitoringHubHtml
2402
+ };