omp-conductor 0.19.1 → 0.19.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omp-conductor",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "A 24/7 dispatcher that takes ready GitHub issues to green, mergeable PRs using omp coding sessions, with tiered escalation first to an orchestrator session and then to a human.",
@@ -29,7 +29,7 @@
29
29
  "LICENSE"
30
30
  ],
31
31
  "scripts": {
32
- "check": "tsc --noEmit && bun run src/check-trailing-newlines.ts",
32
+ "check": "tsc --noEmit && bun run src/check-trailing-newlines.ts && bun run src/check-browser-js.ts",
33
33
  "test": "bun test",
34
34
  "schema": "bun run src/generate-schema.ts"
35
35
  },
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Gate: every browser script this package serves must parse.
3
+ *
4
+ * Run via `bun run check` from the package root, beside the trailing-newline
5
+ * gate. `tsc` never sees `src/dashboard/app.js` — it is plain browser JS,
6
+ * shipped as an asset and loaded by a `<script>` tag — and no test executes
7
+ * it, so nothing in the pipeline had an opinion about whether it was even
8
+ * syntactically valid.
9
+ *
10
+ * It was not. 0.19.0 shipped an `app.js` carrying four `__omp_shell("…")`
11
+ * fragments where `!answer.ok` and `!confirmDestructive(` should have been:
12
+ * the authoring session wrote the file through a Python eval path, whose
13
+ * IPython-style `!cmd` shell escape rewrote every line that began with `!`.
14
+ * A single SyntaxError takes the whole script with it, so the dashboard
15
+ * rendered its static `<h1>` and nothing else — no token prompt, no fleet —
16
+ * and looked, from the outside, like an auth or data problem (#981).
17
+ *
18
+ * A parse is the whole check. It is not a linter and has no opinion about
19
+ * style: it asks the one question a served script must answer yes to.
20
+ */
21
+
22
+ import { execFileSync } from "node:child_process";
23
+ import { readFileSync } from "node:fs";
24
+ import { join } from "node:path";
25
+
26
+ /** Tracked browser scripts, repo-root-relative. */
27
+ export const BROWSER_SCRIPTS: readonly string[] = ["omp/src/dashboard/app.js"];
28
+
29
+ /**
30
+ * The parse error for one script, or null when it parses. Uses Bun's own
31
+ * transpiler: a full parse of the source, without executing a line of it (the
32
+ * script drives a DOM this process does not have).
33
+ */
34
+ export function parseFailure(source: string, path: string): string | null {
35
+ try {
36
+ new Bun.Transpiler({ loader: "js", target: "browser" }).transformSync(source);
37
+ return null;
38
+ } catch (err) {
39
+ const message = err instanceof Error ? err.message : String(err);
40
+ return `${path}: ${message.split("\n")[0]}`;
41
+ }
42
+ }
43
+
44
+ /** One violation line per unparseable script, so a run names every offender. */
45
+ export function findViolations(
46
+ paths: readonly string[],
47
+ read: (path: string) => string,
48
+ ): string[] {
49
+ const violations: string[] = [];
50
+ for (const path of paths) {
51
+ const failure = parseFailure(read(path), path);
52
+ if (failure !== null) violations.push(failure);
53
+ }
54
+ return violations;
55
+ }
56
+
57
+ function repoRoot(): string {
58
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
59
+ }
60
+
61
+ if (import.meta.main) {
62
+ const root = repoRoot();
63
+ const violations = findViolations(BROWSER_SCRIPTS, (path) =>
64
+ readFileSync(join(root, path), "utf8"),
65
+ );
66
+ if (violations.length > 0) {
67
+ for (const violation of violations) console.error(violation);
68
+ console.error(`browser script gate: ${violations.length} script(s) do not parse`);
69
+ process.exit(1);
70
+ }
71
+ console.log("browser script gate: ok");
72
+ }
@@ -140,7 +140,7 @@ async function runControl(path, body, describe) {
140
140
  : JSON.stringify(answer.body);
141
141
  showControlResult(
142
142
  answer.ok ? `${describe}: ok` : `${describe} refused (${answer.status}): ${detail}`,
143
- __omp_shell("answer.ok,")
143
+ !answer.ok,
144
144
  );
145
145
  } catch (err) {
146
146
  showControlResult(`${describe} failed: ${String(err.message ?? err)}`, true);
@@ -188,7 +188,7 @@ controls.addEventListener("click", (event) => {
188
188
  if (reason === null || reason.trim() === "") return;
189
189
  if (
190
190
  action === "hold" &&
191
- __omp_shell("confirmDestructive(")
191
+ !confirmDestructive(
192
192
  "Hold stops new claims AND disarms ticks. Re-arming needs a Telegram challenge answered in the chat. Continue?",
193
193
  )
194
194
  ) {
@@ -447,7 +447,7 @@ function renderRunControls(issue) {
447
447
  const reason = window.prompt(`Reason for stopping #${issue} (recorded in the run's report):`);
448
448
  if (reason === null || reason.trim() === "") return;
449
449
  if (
450
- __omp_shell("confirmDestructive(")
450
+ !confirmDestructive(
451
451
  `Stop #${issue}? This settles the run terminally, salvages its tree and frees the slot. It cannot be resumed.`,
452
452
  )
453
453
  ) {
@@ -474,7 +474,7 @@ function renderRunControls(issue) {
474
474
  );
475
475
  button("Unblock --force", "destructive", () => {
476
476
  if (
477
- __omp_shell("confirmDestructive(")
477
+ !confirmDestructive(
478
478
  `Force-unblock #${issue}? This accepts the loss of uncommitted work in its worktree, which may be the only copy.`,
479
479
  )
480
480
  ) {