@azure-id/orc 0.56.1 → 1.1.0

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 (162) hide show
  1. package/CHANGELOG.md +219 -0
  2. package/README-id.md +49 -107
  3. package/README.md +694 -719
  4. package/bin/build-agents.js +18 -11
  5. package/bin/cli.js +33699 -29855
  6. package/bin/onboarding-content.js +0 -3
  7. package/bin/test-run.js +491 -0
  8. package/bin/verify-contracts.js +4330 -3277
  9. package/bin/verify-package.js +76 -6
  10. package/bin/webui/api.js +1201 -1163
  11. package/bin/webui/app.html +210 -198
  12. package/bin/webui/css/06-responsive.css +120 -108
  13. package/bin/webui/css/panels/extra.css +29 -0
  14. package/bin/webui/css/panels/lanes.css +134 -0
  15. package/bin/webui/css/panels/settings.css +79 -3
  16. package/bin/webui/css/panels/wait.css +123 -0
  17. package/bin/webui/fixtures/extra.js +2036 -1934
  18. package/bin/webui/fixtures/flow.js +2 -2
  19. package/bin/webui/fixtures/index.js +510 -486
  20. package/bin/webui/fixtures/lanes.js +218 -0
  21. package/bin/webui/fixtures/settings.js +261 -34
  22. package/bin/webui/fixtures/stats.js +3 -3
  23. package/bin/webui/fixtures/wait.js +97 -0
  24. package/bin/webui/i18n/TERMS.md +135 -134
  25. package/bin/webui/i18n/en/extra.json +345 -313
  26. package/bin/webui/i18n/en/lanes.json +29 -0
  27. package/bin/webui/i18n/en/nav.json +21 -19
  28. package/bin/webui/i18n/en/overview.json +2 -0
  29. package/bin/webui/i18n/en/settings.json +16 -5
  30. package/bin/webui/i18n/en/wait.json +41 -0
  31. package/bin/webui/i18n/id/extra.json +345 -313
  32. package/bin/webui/i18n/id/lanes.json +29 -0
  33. package/bin/webui/i18n/id/nav.json +21 -19
  34. package/bin/webui/i18n/id/overview.json +2 -0
  35. package/bin/webui/i18n/id/settings.json +15 -4
  36. package/bin/webui/i18n/id/wait.json +41 -0
  37. package/bin/webui/js/01-i18n.js +151 -149
  38. package/bin/webui/js/panels/extra.js +3199 -3011
  39. package/bin/webui/js/panels/lanes.js +259 -0
  40. package/bin/webui/js/panels/overview.js +10 -0
  41. package/bin/webui/js/panels/settings.js +149 -3
  42. package/bin/webui/js/panels/wait.js +253 -0
  43. package/package.json +39 -39
  44. package/templates/agents/MODEL-MAPPING.md +23 -35
  45. package/templates/agents/orc-executor-opus-4-7-high.md +1 -1
  46. package/templates/agents/orc-executor-opus-4-7-med.md +1 -1
  47. package/templates/agents/orc-executor-opus-4-8-high.md +1 -1
  48. package/templates/agents/orc-executor-opus-5-high.md +1 -1
  49. package/templates/agents/orc-executor-opus-5-low.md +1 -1
  50. package/templates/agents/orc-executor-opus-5-med.md +1 -1
  51. package/templates/agents/orc-retro-opus-5-med.md +73 -73
  52. package/templates/agents/orc-retro-sonnet-5-high.md +72 -72
  53. package/templates/agents/orc-trace-writer-haiku-4-5.md +107 -107
  54. package/templates/commands/orc-route.md +1 -1
  55. package/templates/commands/orc-wait.md +19 -0
  56. package/templates/commands/orc.md +1 -1
  57. package/templates/hooks/orc-statusline.js +39 -0
  58. package/templates/skills/_shared/README.md +7 -3
  59. package/templates/skills/_shared/config-precedence.md +198 -0
  60. package/templates/skills/_shared/drift-recovery.md +1 -1
  61. package/templates/skills/_shared/extra-dispatch.md +159 -7
  62. package/templates/skills/_shared/interview.md +1 -1
  63. package/templates/skills/_shared/opus5-only.md +9 -7
  64. package/templates/skills/_shared/phases/README.md +82 -0
  65. package/templates/skills/{orc/references → _shared/phases}/analyst-gates.md +15 -3
  66. package/templates/skills/_shared/phases/execution.md +143 -0
  67. package/templates/skills/{orc/references → _shared/phases}/house-rules.md +13 -1
  68. package/templates/skills/{orc/references → _shared/phases}/intake.md +15 -2
  69. package/templates/skills/_shared/phases/mock-example.md +56 -0
  70. package/templates/skills/{orc/references → _shared/phases}/plan-handoff.md +16 -3
  71. package/templates/skills/_shared/phases/planning.md +135 -0
  72. package/templates/skills/_shared/phases/preflight.md +98 -0
  73. package/templates/skills/_shared/phases/review.md +53 -0
  74. package/templates/skills/_shared/phases/scoring.md +65 -0
  75. package/templates/skills/{orc/references → _shared/phases}/security-checklist.md +12 -1
  76. package/templates/skills/_shared/phases/security.md +55 -0
  77. package/templates/skills/_shared/phases/ship.md +88 -0
  78. package/templates/skills/{orc/references/stop-and-resume.md → _shared/phases/stop-resume.md} +169 -160
  79. package/templates/skills/_shared/phases/summary.md +48 -0
  80. package/templates/skills/_shared/phases/testgen.md +51 -0
  81. package/templates/skills/{orc/references/trace-protocol.md → _shared/phases/trace.md} +49 -6
  82. package/templates/skills/_shared/phases/verify.md +70 -0
  83. package/templates/skills/{orc/references → _shared/phases}/wave-grouping.md +14 -2
  84. package/templates/skills/{orc/references → _shared/phases}/wiki-consult.md +13 -1
  85. package/templates/skills/_shared/read-ladder.md +48 -0
  86. package/templates/skills/_shared/wait.md +240 -0
  87. package/templates/skills/context-combiner/SKILL.md +214 -187
  88. package/templates/skills/orc/README.md +148 -150
  89. package/templates/skills/orc/SKILL.md +75 -358
  90. package/templates/skills/orc/config.md +137 -389
  91. package/templates/skills/orc/examples/full-run-mock.md +1 -1
  92. package/templates/skills/orc/references/effort-and-mode.md +16 -18
  93. package/templates/skills/orc/references/phases/intake.md +47 -0
  94. package/templates/skills/orc/references/phases/integration.md +19 -0
  95. package/templates/skills/orc/references/preflight-report.md +4 -4
  96. package/templates/skills/orc/references/ultra-mode.md +117 -123
  97. package/templates/skills/orc/schemas/planning-output.md +279 -279
  98. package/templates/skills/orc/subskills/orc-planner/SKILL.md +260 -264
  99. package/templates/skills/orc/subskills/orc-pr/stack-gate.md +4 -1
  100. package/templates/skills/orc-aftermath/SKILL.md +158 -136
  101. package/templates/skills/orc-analyze/SKILL.md +252 -220
  102. package/templates/skills/orc-analyze/references/branching.md +51 -51
  103. package/templates/skills/orc-analyze/references/thin-input.md +1 -1
  104. package/templates/skills/orc-analyze-mini/SKILL.md +128 -112
  105. package/templates/skills/orc-boundary/SKILL.md +249 -208
  106. package/templates/skills/orc-brainstorm/SKILL.md +377 -339
  107. package/templates/skills/orc-budget/SKILL.md +248 -225
  108. package/templates/skills/orc-challenge/README.md +1 -1
  109. package/templates/skills/orc-challenge/SKILL.md +298 -255
  110. package/templates/skills/orc-challenge/examples/council-full-roster.md +1 -1
  111. package/templates/skills/orc-claude/SKILL.md +221 -199
  112. package/templates/skills/orc-diy/README.md +2 -1
  113. package/templates/skills/orc-diy/SKILL.md +132 -68
  114. package/templates/skills/orc-diy/references/blocks/analyze.md +1 -1
  115. package/templates/skills/orc-diy/references/compile.md +21 -7
  116. package/templates/skills/orc-diy/references/flow-schema.md +2 -2
  117. package/templates/skills/orc-doc/SKILL.md +49 -7
  118. package/templates/skills/orc-explain/SKILL.md +24 -0
  119. package/templates/skills/orc-export/SKILL.md +31 -5
  120. package/templates/skills/orc-fast/SKILL.md +218 -185
  121. package/templates/skills/orc-grill/SKILL.md +238 -207
  122. package/templates/skills/orc-handoff/SKILL.md +223 -204
  123. package/templates/skills/orc-learn/SKILL.md +181 -156
  124. package/templates/skills/orc-mini/SKILL.md +47 -24
  125. package/templates/skills/orc-pact/SKILL.md +255 -218
  126. package/templates/skills/orc-pattern/SKILL.md +154 -124
  127. package/templates/skills/orc-poly/SKILL.md +242 -216
  128. package/templates/skills/orc-poly/references/gather.md +1 -1
  129. package/templates/skills/orc-pr-driver/SKILL.md +196 -167
  130. package/templates/skills/orc-pr-driver/references/orc-run-split.md +1 -1
  131. package/templates/skills/orc-pr-setup/SKILL.md +212 -184
  132. package/templates/skills/orc-quick/README.md +2 -2
  133. package/templates/skills/orc-quick/SKILL.md +51 -13
  134. package/templates/skills/orc-quick/references/dispatch-gate.md +2 -3
  135. package/templates/skills/orc-retro/SKILL.md +249 -222
  136. package/templates/skills/orc-retro/examples/retro-mock.md +171 -171
  137. package/templates/skills/orc-route/SKILL.md +188 -165
  138. package/templates/skills/orc-verify/SKILL.md +103 -84
  139. package/templates/skills/orc-wait/SKILL.md +163 -0
  140. package/templates/skills/orc-wiki/SKILL.md +60 -206
  141. package/templates/skills/orc-wiki/references/phases/phase-0.md +71 -0
  142. package/templates/skills/orc-wiki/references/phases/phase-1.md +35 -0
  143. package/templates/skills/orc-wiki/references/phases/phase-2.md +52 -0
  144. package/templates/skills/orc-wiki/references/phases/phase-3.md +57 -0
  145. package/templates/skills/orc-wiki/references/phases/phase-3c.md +36 -0
  146. package/templates/agents/orc-advisor-fable-5.md +0 -50
  147. package/templates/agents/orc-analyst-fable-5.md +0 -115
  148. package/templates/agents/orc-judge-fable-5.md +0 -79
  149. package/templates/agents/orc-planner-fable-5.md +0 -152
  150. package/templates/agents/orc-reviewer-fable-5.md +0 -57
  151. package/templates/skills/_shared/fable5-override.md +0 -56
  152. package/templates/skills/orc-diy/references/blocks/execution.md +0 -42
  153. package/templates/skills/orc-diy/references/blocks/mock-example.md +0 -22
  154. package/templates/skills/orc-diy/references/blocks/planning.md +0 -34
  155. package/templates/skills/orc-diy/references/blocks/review.md +0 -18
  156. package/templates/skills/orc-diy/references/blocks/scoring.md +0 -16
  157. package/templates/skills/orc-diy/references/blocks/security.md +0 -25
  158. package/templates/skills/orc-diy/references/blocks/ship.md +0 -25
  159. package/templates/skills/orc-diy/references/blocks/summary.md +0 -18
  160. package/templates/skills/orc-diy/references/blocks/testgen.md +0 -17
  161. package/templates/skills/orc-diy/references/blocks/trace.md +0 -28
  162. package/templates/skills/orc-diy/references/blocks/verify.md +0 -25
@@ -116,9 +116,6 @@ const SECTIONS = [
116
116
  "Groups:",
117
117
  " Common waves, pauses, scoring granularity, analysis depth, tests,",
118
118
  " pattern gate, security pass, stacked PRs (stacked_pr + thresholds)",
119
- " Fable 5 fable5_enabled / fable5_effort / fable5_roles — HARD-GATED role",
120
- " override: route analyze/plan/advisor/judge/review to Fable 5 agents",
121
- " (nothing happens unless fable5_enabled: true AND roles are selected)",
122
119
  " Advanced dirs, crosslink day-tiers, orchestrator model, log dir",
123
120
  ],
124
121
  },
@@ -0,0 +1,491 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // ORC's test runner — POOLS, not shards. (v1.0.0 W0; design-07.)
4
+ //
5
+ // WHY THIS EXISTS. `npm test` used to be `node --test --test-concurrency=6
6
+ // test/`: one global concurrency for 41 files with four completely different
7
+ // resource appetites. 250 of 597 tests cost 36 s between them and were being
8
+ // throttled to protect ~12 files that spawn real child processes, stand up a
9
+ // loopback server, or derive scrypt at N=2^17. One number cannot serve both,
10
+ // and the number that keeps the badly-behaved files honest is the number the
11
+ // whole suite ran at. `test/_helpers.js` carries the full scar: the peak came
12
+ // 8 -> 6 after a full run failed TWENTY-NINE tests that all passed on their own
13
+ // file. That comment is still the REASON these numbers exist; it now points
14
+ // here for the numbers themselves.
15
+ //
16
+ // So: every file DECLARES its resource class in a first-lines pragma
17
+ //
18
+ // // @test-pool net — stands up the fake provider on loopback
19
+ //
20
+ // and files run pool by pool, each pool at its own concurrency. A class is
21
+ // never sniffed from the source: a grep would be wrong the first time somebody
22
+ // puts a spawnSync inside a helper, which is a debugging round this repo has
23
+ // already paid for once (test/cli/_fake-provider.js).
24
+ //
25
+ // This script schedules; `node --test` still does the testing. One child per
26
+ // file — which is what `node --test` does internally anyway — so the pass count
27
+ // is identical to the old command, per-file durations are exact, and a failure
28
+ // prints ONLY the failing file's output instead of 3,000 lines of TAP.
29
+ //
30
+ // THE GUARD THAT MATTERS: a selective run is NEVER the gate. `--file`,
31
+ // `--pool` and `--since` all set `is_gate: false`, name every file they skipped
32
+ // in the JSON, and print a loud last line. A green partial run reported as a
33
+ // green suite is the worst possible outcome of making a suite faster.
34
+ // There is no retry-until-green here, at any level: a flake is RECORDED.
35
+ const fs = require("fs");
36
+ const os = require("os");
37
+ const path = require("path");
38
+ const { spawn, spawnSync } = require("child_process");
39
+
40
+ // Run start, so every file reports WHEN it ran and not only for how long.
41
+ let T0 = Date.now();
42
+ const REPO = path.join(__dirname, "..");
43
+ const TEST_DIR = path.join(REPO, "test");
44
+ const TIMINGS = path.join(TEST_DIR, ".timings.json");
45
+
46
+ // ── the pools ───────────────────────────────────────────────────────────────
47
+ // Concurrency numbers here are EMPIRICAL, not chosen. Raising one requires
48
+ // three clean runs of that pool (design-07 §5.3) — the v0.55.0 scar again: 8
49
+ // was "already marginal" and only became visible when two files were added.
50
+ const CPUS = os.cpus().length || 4;
51
+ const POOLS = {
52
+ // Pure computation: reads files, greps the payload, evaluates pure functions
53
+ // out of cli.js. Over-subscription cannot hurt these.
54
+ pure: { concurrency: Math.max(2, CPUS - 2), why: "no child process, no server, no crypto, no real clock" },
55
+ // A child process is memory and a scheduler slot, not a socket.
56
+ spawn: { concurrency: 4, why: "shells `node bin/cli.js` or an installed hook" },
57
+ // THE STARVATION CLASS. A real timeout on a loopback socket is what breaks
58
+ // when these are crowded, and it breaks looking exactly like a regression.
59
+ net: { concurrency: 2, why: "stands up a fake provider, a fake CLI or the ui server on loopback" },
60
+ // scrypt at N=2^17 is 128 MB and a beat of wall clock, ON PURPOSE — that cost
61
+ // IS the vault's defence and must never be tuned down. It gets a lane to
62
+ // itself instead.
63
+ heavy: { concurrency: 1, why: "derives scrypt at N=2^17" },
64
+ };
65
+ const POOL_ORDER = ["pure", "spawn", "net", "heavy"];
66
+ // THE POOLS OVERLAP, and the correction is worth writing down (v1.0.0 W0).
67
+ //
68
+ // design-07 §3 had the pools run sequentially, one after another. Built that
69
+ // way and MEASURED, the full gate took 504 s against the old command's 328 s —
70
+ // slower, because a pool boundary serialises the whole box: `net` at 2 left 14
71
+ // cores idle for three minutes. The design's arithmetic assumed the cost was
72
+ // where the TEST durations were (250 free tests, 36 s), and the cost is
73
+ // actually per-FILE wall clock — 1,037 s of it, most of it node startup and
74
+ // fixture setup inside files that spawn.
75
+ //
76
+ // So the pools run TOGETHER, under one global cap, each still holding its own
77
+ // per-class limit. That keeps the thesis exactly — `net` is never more than 2
78
+ // and `heavy` is never more than 1 — and it is strictly SAFER than the old
79
+ // single number, under which all 6 concurrent processes could be `net` files.
80
+ // The global cap is what stops 14 + 4 + 2 + 1 becoming 21 processes.
81
+ // v1.0.0 W8 — a MEASUREMENT SEAM, not a behaviour change. Unset, this is 8 and
82
+ // the runner is byte-identical to what shipped (the `ORC_TEST_BUDGET_FLOOR_MS`
83
+ // precedent from W0/D22).
84
+ //
85
+ // It exists because the gate itself became unreliable. The full suite flaked on
86
+ // UNCHANGED code in three consecutive waves — W6 (12 failures, detail lost),
87
+ // W7 (4), W8 (3, then 13) — always in the `spawn`/`net` pools, always with
88
+ // ~24 s durations that are a budget elapsing rather than an assertion failing,
89
+ // and always on a run 35-90% slower than the ~188 s norm. Every affected file
90
+ // passes in isolation.
91
+ //
92
+ // Measured on the same tree, same commit: at 8 the suite failed 13 tests in
93
+ // 358 s; at 4 it passed 653/653 in 302 s. SLOWER at higher concurrency is the
94
+ // contention signature — the box is oversubscribed, so real timeouts elapse.
95
+ //
96
+ // ANSWERED at v1.0.0 W11: the default is 4, on that measurement. A cap that is
97
+ // both slower and red is not buying anything, and a green run at 8 on an idle
98
+ // box (W9, W10) is evidence about the box rather than about the cap. The seam
99
+ // stays — `ORC_TEST_GLOBAL_CONCURRENCY=8` reproduces the old behaviour exactly.
100
+ const GLOBAL_CONCURRENCY = Number(process.env.ORC_TEST_GLOBAL_CONCURRENCY) || 4;
101
+ // A file with no pragma lands in the safe middle AND is reported by name.
102
+ const DEFAULT_POOL = "spawn";
103
+
104
+ // ── which tests a source change can break ───────────────────────────────────
105
+ // A DECLARED table, never an inference. A changed path that matches no rule
106
+ // runs EVERYTHING — the fail-safe direction. An engine that quietly picks three
107
+ // files is how a suite stops testing things.
108
+ const TEST_TOUCHES = [
109
+ [/^bin\/cli\.js$/, ["test/cli/", "test/lanes/", "test/webui/api.test.js", "test/webui/serve.test.js"]],
110
+ [/^bin\/webui\//, ["test/webui/"]],
111
+ [/^bin\/pricing\.json$/, ["test/cli/extra-stats.test.js"]],
112
+ [/^bin\/providers\.json$/, ["test/cli/extra-tools.test.js", "test/cli/extra-routing.test.js"]],
113
+ [/^bin\/mockrun-catalog\.js$/, ["test/cli/knowledge.test.js", "test/docs.test.js"]],
114
+ [/^bin\/verify-/, ["test/payload.test.js"]],
115
+ [/^templates\/hooks\//, ["test/hooks.test.js"]],
116
+ [/^templates\/agents\//, ["test/payload.test.js"]],
117
+ [/^templates\/(skills|commands)\//, ["test/payload.test.js", "test/docs.test.js"]],
118
+ // The lane suites grep the payload of the lane they are named after, so a
119
+ // skill edit inside one of these directories widens to its own suite. This is
120
+ // the ONLY inference in the table and it is a name match, not a guess.
121
+ [/^templates\/(skills|commands)\/orc-doc/, ["test/lanes/doc.test.js"]],
122
+ [/^templates\/(skills|commands)\/orc-challenge/, ["test/lanes/challenge.test.js", "test/lanes/council.test.js"]],
123
+ [/^templates\/skills\/_shared\//, ["test/lanes/"]],
124
+ [/^test\//, ["SELF"]],
125
+ [/^(README|README-id|CHANGELOG)\.md$/, ["test/docs.test.js"]],
126
+ ];
127
+
128
+ function walk(dir, out) {
129
+ for (const e of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1))) {
130
+ const p = path.join(dir, e.name);
131
+ if (e.isDirectory()) walk(p, out);
132
+ else if (e.name.endsWith(".js")) out.push(path.relative(REPO, p).split(path.sep).join("/"));
133
+ }
134
+ return out;
135
+ }
136
+
137
+ // The pragma is read from the first lines only: a `@test-pool` mentioned in
138
+ // prose 400 lines down is documentation, not a declaration.
139
+ function poolOf(rel) {
140
+ const head = fs.readFileSync(path.join(REPO, rel), "utf8").split(/\r?\n/).slice(0, 8).join("\n");
141
+ const m = /@test-pool\s+(\w+)/.exec(head);
142
+ if (!m) return { pool: DEFAULT_POOL, declared: false };
143
+ if (!POOLS[m[1]]) return { pool: DEFAULT_POOL, declared: false, bad: m[1] };
144
+ return { pool: m[1], declared: true };
145
+ }
146
+
147
+ function readTimings() {
148
+ try {
149
+ return JSON.parse(fs.readFileSync(TIMINGS, "utf8"));
150
+ } catch (_) {
151
+ return {};
152
+ }
153
+ }
154
+
155
+ // ── TAP parsing ─────────────────────────────────────────────────────────────
156
+ // Only the trailing counters are read. The per-test lines are kept as raw
157
+ // output and printed only when the file fails.
158
+ function counters(tap) {
159
+ const num = (k) => {
160
+ const m = new RegExp("^# " + k + " (\\d+)$", "m").exec(tap);
161
+ return m ? +m[1] : 0;
162
+ };
163
+ return { tests: num("tests"), pass: num("pass"), fail: num("fail"), skipped: num("skipped"), todo: num("todo") };
164
+ }
165
+
166
+ // THE FAILING TESTS, BY NAME AND BY DURATION (v1.0.0 W15).
167
+ //
168
+ // `--json is not a summary` (plan sec5 rule 10, which says in terms that it
169
+ // binds this file). Until W15 the human branch printed a failing file's WHOLE
170
+ // TAP - every test name, its duration and the assertion - and `--json` carried
171
+ // `{file, status, fail}`. A field the human path prints and the JSON omits is
172
+ // drift no lint can see, because both halves live in one function.
173
+ //
174
+ // It was not a theoretical gap. W14 recorded four full runs with four different
175
+ // failure sets and could not say WHICH tests failed or how long they took -
176
+ // which is precisely the evidence that separates a budget elapsing from an
177
+ // assertion failing, the two hypotheses that were open. Four runs, no durations
178
+ // kept, and the wave had to hand the question on.
179
+ function failedTests(tap) {
180
+ const out = [];
181
+ const re = /^not ok \d+ - (.+)$((?:\n[ ]+.*)*)/gm;
182
+ let m;
183
+ while ((m = re.exec(tap))) {
184
+ const body = m[2] || "";
185
+ const dur = /duration_ms:\s*(\d+(?:\.[0-9]+)?)/.exec(body);
186
+ const loc = /location:\s*'([^']*)'/.exec(body);
187
+ // The FIRST line of the error is the assertion. The YAML diff under it is
188
+ // for the human branch; a report field that carried it would be a wall of
189
+ // text in every consumer.
190
+ const err = /error:\s*[|-]*\s*'?([^\n]*)/.exec(body);
191
+ out.push({
192
+ name: m[1].trim(),
193
+ duration_s: dur ? +(Number(dur[1]) / 1000).toFixed(1) : null,
194
+ location: loc ? loc[1] : null,
195
+ error: err ? err[1].replace(/'$/, "").trim().slice(0, 300) : null,
196
+ });
197
+ }
198
+ return out;
199
+ }
200
+
201
+ function runFile(rel) {
202
+ return new Promise((resolve) => {
203
+ const started = Date.now();
204
+ const child = spawn(process.execPath, ["--test", "--test-reporter=tap", rel], {
205
+ cwd: REPO,
206
+ env: { ...process.env, ORC_NO_UPDATE_CHECK: "1" },
207
+ stdio: ["ignore", "pipe", "pipe"],
208
+ });
209
+ let out = "";
210
+ let err = "";
211
+ child.stdout.on("data", (d) => (out += d));
212
+ child.stderr.on("data", (d) => (err += d));
213
+ child.on("close", (code) => {
214
+ const c = counters(out);
215
+ resolve({
216
+ file: rel,
217
+ ms: Date.now() - started,
218
+ // WHEN a file ran, not only how long it took. Two loopback files being
219
+ // in flight together IS the contention question, and a report with no
220
+ // start offset cannot answer it however many times it is re-run.
221
+ started_at: started,
222
+ failed_tests: failedTests(out),
223
+ status: code,
224
+ ok: code === 0,
225
+ ...c,
226
+ output: out + (err ? "\n--- stderr ---\n" + err : ""),
227
+ });
228
+ });
229
+ });
230
+ }
231
+
232
+ // ONE scheduler over every pool at once. Longest-job-first from the last run's
233
+ // timings — a file with no recorded time sorts FIRST (it is unknown, and
234
+ // unknown is not fast); ties fall back to the sorted path, so a failure
235
+ // reproduces. A file starts only when its pool has a free slot AND the global
236
+ // cap has one, so the box stays busy without any class exceeding its limit.
237
+ async function runAll(byPool, timings, opts) {
238
+ const cost = (f) => (timings[f] === undefined ? Infinity : timings[f]);
239
+ const queue = [];
240
+ for (const name of POOL_ORDER) for (const f of byPool[name] || []) queue.push({ file: f, pool: name });
241
+ queue.sort((a, b) => cost(b.file) - cost(a.file) || (a.file < b.file ? -1 : 1));
242
+
243
+ const inPool = {};
244
+ const results = [];
245
+ const poolMs = {};
246
+ const poolStart = {};
247
+ let running = 0;
248
+ let idx = 0;
249
+
250
+ await new Promise((done) => {
251
+ const pump = () => {
252
+ while (running < GLOBAL_CONCURRENCY) {
253
+ // Scan for the most expensive job whose pool has room. Skipping a
254
+ // blocked job rather than stalling on it is what lets `pure` drain
255
+ // while `net`'s two slots are busy.
256
+ let pick = -1;
257
+ for (let i = idx; i < queue.length; i++) {
258
+ const j = queue[i];
259
+ if (j.done) continue;
260
+ if ((inPool[j.pool] || 0) < POOLS[j.pool].concurrency) {
261
+ pick = i;
262
+ break;
263
+ }
264
+ }
265
+ if (pick < 0) break;
266
+ const job = queue[pick];
267
+ job.done = true;
268
+ while (idx < queue.length && queue[idx].done) idx++;
269
+ inPool[job.pool] = (inPool[job.pool] || 0) + 1;
270
+ if (poolStart[job.pool] === undefined) poolStart[job.pool] = Date.now();
271
+ running++;
272
+ runFile(job.file).then((r) => {
273
+ results.push({ ...r, pool: job.pool });
274
+ poolMs[job.pool] = Date.now() - poolStart[job.pool];
275
+ inPool[job.pool]--;
276
+ running--;
277
+ if (!opts.json) process.stdout.write(r.ok ? "." : "\n FAIL " + r.file + "\n");
278
+ if (results.length === queue.length) return done();
279
+ pump();
280
+ });
281
+ }
282
+ if (!queue.length) done();
283
+ };
284
+ pump();
285
+ });
286
+
287
+ return POOL_ORDER.filter((n) => (byPool[n] || []).length).map((n) => ({
288
+ pool: n,
289
+ concurrency: POOLS[n].concurrency,
290
+ files: results.filter((r) => r.pool === n).sort((a, b) => (a.file < b.file ? -1 : 1)),
291
+ ms: poolMs[n] || 0,
292
+ }));
293
+ }
294
+
295
+ // ── selection ───────────────────────────────────────────────────────────────
296
+ function changedPaths(ref) {
297
+ const args = ["diff", "--name-only", ref === true ? "HEAD" : ref];
298
+ const a = spawnSync("git", args, { cwd: REPO, encoding: "utf8" });
299
+ const b = spawnSync("git", ["ls-files", "--others", "--exclude-standard"], { cwd: REPO, encoding: "utf8" });
300
+ if (a.status !== 0) return null;
301
+ return (a.stdout + (b.stdout || "")).split(/\r?\n/).filter(Boolean);
302
+ }
303
+
304
+ function selectSince(all, paths) {
305
+ const picked = new Set();
306
+ for (const p of paths) {
307
+ let matched = false;
308
+ for (const [re, targets] of TEST_TOUCHES) {
309
+ if (!re.test(p)) continue;
310
+ matched = true;
311
+ for (const t of targets) {
312
+ if (t === "SELF") {
313
+ if (all.includes(p)) picked.add(p);
314
+ } else for (const f of all) if (f === t || f.startsWith(t)) picked.add(f);
315
+ }
316
+ }
317
+ // The fail-safe direction: a path no rule claims runs the whole suite.
318
+ if (!matched) return { files: all, widened_by: p };
319
+ }
320
+ return { files: all.filter((f) => picked.has(f)), widened_by: null };
321
+ }
322
+
323
+ function usage(msg) {
324
+ if (msg) console.error("test-run: " + msg);
325
+ console.error(
326
+ [
327
+ "",
328
+ "usage: node bin/test-run.js [--pool <name>] [--file <substr>] [--since [ref]] [--json]",
329
+ "",
330
+ " (no flags) run everything, pool by pool. THIS is the gate.",
331
+ " --pool <name> one pool only: " + POOL_ORDER.join(" | "),
332
+ " --file <substr> files whose path contains <substr>",
333
+ " --since [ref] files a working-tree change against <ref> (default HEAD) can break",
334
+ " --json the whole computed object on stdout, nothing else",
335
+ "",
336
+ "exit: 0 all green · 1 a test failed · 2 a bad argument",
337
+ "",
338
+ ].join("\n")
339
+ );
340
+ return 2;
341
+ }
342
+
343
+ async function main(argv) {
344
+ const opts = { json: false, pool: null, file: null, since: null };
345
+ for (let i = 0; i < argv.length; i++) {
346
+ const a = argv[i];
347
+ if (a === "--json") opts.json = true;
348
+ else if (a === "--pool") {
349
+ opts.pool = argv[++i];
350
+ if (!POOLS[opts.pool]) return usage("unknown pool: " + opts.pool);
351
+ } else if (a === "--file") {
352
+ opts.file = argv[++i];
353
+ if (!opts.file) return usage("--file needs a substring");
354
+ } else if (a === "--since") {
355
+ opts.since = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : true;
356
+ } else if (a === "-h" || a === "--help") return usage();
357
+ else return usage("unknown argument: " + a);
358
+ }
359
+
360
+ const all = walk(TEST_DIR, []);
361
+ const classified = {};
362
+ const unclassified = [];
363
+ const misdeclared = [];
364
+ for (const f of all) {
365
+ const c = poolOf(f);
366
+ classified[f] = c.pool;
367
+ if (!c.declared) (c.bad ? misdeclared : unclassified).push(c.bad ? f + " (@test-pool " + c.bad + ")" : f);
368
+ }
369
+
370
+ let selected = all;
371
+ let widened_by = null;
372
+ const is_gate = !opts.pool && !opts.file && !opts.since;
373
+ if (opts.file) selected = all.filter((f) => f.includes(opts.file));
374
+ if (opts.since) {
375
+ const paths = changedPaths(opts.since);
376
+ if (!paths) return usage("--since needs a git repository");
377
+ const s = selectSince(selected, paths);
378
+ selected = s.files;
379
+ widened_by = s.widened_by;
380
+ }
381
+ if (opts.pool) selected = selected.filter((f) => classified[f] === opts.pool);
382
+
383
+ const timings = readTimings();
384
+ const byPool = {};
385
+ for (const name of POOL_ORDER) {
386
+ const files = selected.filter((f) => classified[f] === name);
387
+ if (files.length) byPool[name] = files;
388
+ }
389
+ if (!opts.json && selected.length)
390
+ console.log(
391
+ "running " +
392
+ selected.length +
393
+ " files, global x" +
394
+ GLOBAL_CONCURRENCY +
395
+ " — " +
396
+ POOL_ORDER.filter((n) => byPool[n])
397
+ .map((n) => n + " x" + POOLS[n].concurrency + " (" + byPool[n].length + ")")
398
+ .join(" · ")
399
+ );
400
+ const t0 = Date.now();
401
+ T0 = t0;
402
+ const pools = await runAll(byPool, timings, opts);
403
+ const wall_ms = Date.now() - t0;
404
+ if (!opts.json && selected.length) process.stdout.write("\n");
405
+
406
+ const files = pools.flatMap((p) => p.files);
407
+ const totals = files.reduce(
408
+ (t, f) => ({ tests: t.tests + f.tests, pass: t.pass + f.pass, fail: t.fail + f.fail, skipped: t.skipped + f.skipped, todo: t.todo + f.todo }),
409
+ { tests: 0, pass: 0, fail: 0, skipped: 0, todo: 0 }
410
+ );
411
+ const failed = files.filter((f) => !f.ok);
412
+ const skippedFiles = all.filter((f) => !selected.includes(f));
413
+
414
+ // Longest-job-first needs a record. Written on every run, gate or not;
415
+ // a selective run only refreshes the files it actually ran.
416
+ const nextTimings = { ...timings };
417
+ for (const f of files) nextTimings[f.file] = f.ms;
418
+ try {
419
+ fs.writeFileSync(TIMINGS, JSON.stringify(nextTimings, null, 1) + "\n");
420
+ } catch (_) {}
421
+
422
+ const report = {
423
+ ok: failed.length === 0,
424
+ // A selective run is NEVER the gate. This field is the machine-readable
425
+ // half of that rule and the last line below is the human half.
426
+ is_gate,
427
+ wall_clock_s: +(wall_ms / 1000).toFixed(1),
428
+ global_concurrency: GLOBAL_CONCURRENCY,
429
+ totals,
430
+ files_run: selected.length,
431
+ files_total: all.length,
432
+ // A file not run is NAMED, always.
433
+ skipped_files: skippedFiles,
434
+ widened_by,
435
+ unclassified,
436
+ misdeclared,
437
+ // A flake is RECORDED, never retried away: nothing in this runner re-runs a
438
+ // failure, so this array is filled only by a human recording one.
439
+ flaky: [],
440
+ pools: pools.map((p) => ({
441
+ pool: p.pool,
442
+ concurrency: p.concurrency,
443
+ wall_clock_s: +(p.ms / 1000).toFixed(1),
444
+ why: POOLS[p.pool].why,
445
+ files: p.files.map((f) => ({ file: f.file, ms: f.ms, started_s: +((f.started_at - T0) / 1000).toFixed(1), tests: f.tests, pass: f.pass, fail: f.fail, ok: f.ok })),
446
+ })),
447
+ failures: failed.map((f) => ({ file: f.file, status: f.status, fail: f.fail, pool: f.pool, ms: f.ms, started_s: +((f.started_at - T0) / 1000).toFixed(1), tests: f.failed_tests })),
448
+ };
449
+
450
+ if (opts.json) {
451
+ process.stdout.write(JSON.stringify(report, null, 1) + "\n");
452
+ return failed.length ? 1 : 0;
453
+ }
454
+
455
+ // Failure-only reporting. 3,000 lines of TAP for one ECONNRESET is why
456
+ // nobody reads the current output.
457
+ for (const f of failed) {
458
+ console.log("\n" + "=".repeat(72) + "\n" + f.file + " — exit " + f.status + "\n" + "=".repeat(72));
459
+ console.log(f.output.trim());
460
+ }
461
+ if (unclassified.length) console.log("\nunclassified (ran in `" + DEFAULT_POOL + "`): " + unclassified.join(", "));
462
+ if (misdeclared.length) console.log("\nBAD @test-pool pragma (ran in `" + DEFAULT_POOL + "`): " + misdeclared.join(", "));
463
+ console.log(
464
+ "\n" +
465
+ (failed.length ? "FAIL" : "ok") +
466
+ " — " +
467
+ totals.pass +
468
+ " passed, " +
469
+ totals.fail +
470
+ " failed, " +
471
+ totals.tests +
472
+ " tests in " +
473
+ (wall_ms / 1000).toFixed(1) +
474
+ "s across " +
475
+ selected.length +
476
+ " files"
477
+ );
478
+ if (!is_gate)
479
+ console.log(
480
+ "ran " + selected.length + " of " + all.length + " files — this is NOT the gate" + (widened_by ? " (widened to everything by " + widened_by + ")" : "")
481
+ );
482
+ return failed.length ? 1 : 0;
483
+ }
484
+
485
+ main(process.argv.slice(2)).then(
486
+ (code) => process.exit(code),
487
+ (e) => {
488
+ console.error(e && e.stack ? e.stack : String(e));
489
+ process.exit(2);
490
+ }
491
+ );