scripts-orchestrator 2.13.0 → 2.15.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.
package/README.md CHANGED
@@ -36,6 +36,11 @@ npm install --save-dev scripts-orchestrator
36
36
  - **Optional Phases**: Mark phases as optional and run them selectively
37
37
  - **Git-Based Caching**: Automatically skips execution when git state is unchanged
38
38
  - **Comprehensive Logging**: Detailed logging of command execution and results
39
+ - **Incremental JSON results**: Live-updating `json_results` file as commands complete (v2.14+)
40
+ - **NDJSON event stream**: Machine-readable per-command events for dashboards (v2.14+)
41
+ - **Post-run hook**: Run a shell command after results are written via `post_run` config (v2.14+)
42
+ - **Run-state file**: Library-owned in-progress indicator for live dashboard integration (v2.14+)
43
+ - **Phase recommendations**: Memory-aware `--recommend` mode that proposes an optimal phase layout from a run's time/memory metrics (advisory, v2.15+)
39
44
 
40
45
  ## Configuration
41
46
 
@@ -410,6 +415,84 @@ export default {
410
415
 
411
416
  All logs (command logs, main orchestrator logs, and git cache) will be stored in the specified folder.
412
417
 
418
+ ## Live Dashboard Integration (v2.14+)
419
+
420
+ ### Incremental JSON results
421
+
422
+ By default `json_results` is written only at the end of a run. From v2.14 onward the file is
423
+ updated atomically (write-to-temp + rename) after **each command starts or completes**, so
424
+ watchers always see a consistent snapshot:
425
+
426
+ ```json
427
+ {
428
+ "success": null,
429
+ "timestamp": "2026-06-04T07:13:21.000Z",
430
+ "commands": [
431
+ { "command": "lint-ci", "phase": "lint", "success": true, "durationMs": 4200 },
432
+ { "command": "playwright_ci", "phase": "tests", "success": null, "startedAt": "2026-06-04T07:13:25.000Z" }
433
+ ]
434
+ }
435
+ ```
436
+
437
+ `"success": null` at the top level is the in-progress sentinel. It is replaced with `true` or
438
+ `false` when `writeJsonResults` writes the final result.
439
+
440
+ ### NDJSON event stream
441
+
442
+ Alongside `json_results`, the library writes one NDJSON line per event to
443
+ `<json_results_basename>-events.ndjson`:
444
+
445
+ ```jsonl
446
+ {"type":"command_start","timestamp":"...","command":"lint-ci","phase":"lint","scope":"workspace"}
447
+ {"type":"command_end","timestamp":"...","command":"lint-ci","phase":"lint","success":true,"durationMs":4200}
448
+ {"type":"run_end","timestamp":"...","success":true,"durationMs":12800}
449
+ ```
450
+
451
+ Dashboard tools can `tail -f` this file or watch it with `fs.watch` to get real-time updates
452
+ without parsing human-readable log lines.
453
+
454
+ ### Run-state file (A4)
455
+
456
+ When `--logFolder` is specified, the library writes `{logFolder}/.scripts-orchestrator-run.json`
457
+ at run start and removes it on run end:
458
+
459
+ ```json
460
+ {
461
+ "startedAt": "2026-06-04T07:13:17.000Z",
462
+ "pid": 12345,
463
+ "phase": "tests",
464
+ "activeCommand": "playwright_ci"
465
+ }
466
+ ```
467
+
468
+ This file is the authoritative in-progress signal for live dashboards. Its absence means the run
469
+ has finished (or never started).
470
+
471
+ ### Post-run hook (A7)
472
+
473
+ Add `post_run` to your config to run a shell command **after** `json_results` is written and the
474
+ run-state file is cleared, but **before** `process.exit()`:
475
+
476
+ ```javascript
477
+ export default {
478
+ json_results: './logs/scripts-orchestrator-results.json',
479
+ post_run: 'node scripts/generate-report.js', // called after every run
480
+ phases: [ /* ... */ ]
481
+ };
482
+ ```
483
+
484
+ The hook receives two environment variables:
485
+ - `SCRIPTS_ORCHESTRATOR_SUCCESS=1` (or `0`) — whether the run succeeded
486
+ - `SCRIPTS_ORCHESTRATOR_EXIT_CODE=0` (or `1`) — same, as a numeric exit code
487
+
488
+ The hook runs synchronously and its exit code is logged but does not change the orchestrator's
489
+ own exit code.
490
+
491
+ **Typical use case:** trigger a monorepo rollup report after each workspace finishes:
492
+ ```javascript
493
+ post_run: 'node ../../scripts/merge-orchestrator-report.js'
494
+ ```
495
+
413
496
  ## Git-Based Caching
414
497
 
415
498
  The orchestrator automatically tracks the git commit hash and repository state to optimize execution:
@@ -440,6 +523,50 @@ This is useful when you want to:
440
523
  - Debug issues without modifying the codebase
441
524
  - Override the cache in CI/CD pipelines
442
525
 
526
+ ## Phase Recommendations (advisory)
527
+
528
+ When a run is executed with `metrics: ['time', 'memory']`, the results JSON records each command's
529
+ `durationMs` and peak `memoryKb`. The `--recommend` mode reads that JSON and prints a **memory-aware
530
+ phase recommendation**: it never runs anything and writes no files.
531
+
532
+ ```bash
533
+ # Analyse an existing results JSON and print a suggested phase layout
534
+ scripts-orchestrator --recommend ./logs/scripts-orchestrator-results.json
535
+
536
+ # Size the budget for a machine running N gates in parallel (each gets 1/N of RAM and cores)
537
+ scripts-orchestrator --recommend ./logs/scripts-orchestrator-results.json --fanout 3
538
+
539
+ # Override the memory budget explicitly (MB) or change the RAM safety fraction
540
+ scripts-orchestrator --recommend ./logs/results.json --budget-mb 8192
541
+ scripts-orchestrator --recommend ./logs/results.json --mem-safety 0.7
542
+ ```
543
+
544
+ It reports two things:
545
+
546
+ 1. **Observed timeline** — each phase's wall-clock (the longest step in it) and the concurrent peak
547
+ memory (Σ of member peaks), flagging any phase whose concurrent peak exceeds the host budget.
548
+ 2. **Recommended layout** — a [First-Fit-Decreasing](https://en.wikipedia.org/wiki/Bin_packing_problem)
549
+ bin-packing by duration that groups steps into sequential phases so each phase's concurrent peak
550
+ memory stays under `budget = totalmem × memSafety ÷ fanout` and its step count stays under
551
+ `coreShare = (cores − 2) ÷ fanout`. Long steps seed phases; short steps fill the gaps beneath them,
552
+ so the estimated makespan stays near the theoretical floor (the single longest step) without
553
+ oversubscribing RAM.
554
+
555
+ The same logic is exported for programmatic use:
556
+
557
+ ```js
558
+ import { recommendPhases, formatRecommendationReport } from 'scripts-orchestrator';
559
+
560
+ const payload = JSON.parse(fs.readFileSync('./logs/results.json', 'utf8'));
561
+ const rec = recommendPhases(payload, { fanout: 3 });
562
+ console.log(formatRecommendationReport(rec));
563
+ // rec.recommended.bins, rec.observed, rec.observedMakespanMs, rec.budgetBytes, …
564
+ ```
565
+
566
+ This is advisory only — the budget is conservative (per-process peaks summed as if they coincide) and
567
+ the packing does not model inter-phase data dependencies, so validate any suggested layout against a
568
+ real run before adopting it.
569
+
443
570
  ## Exit Codes
444
571
 
445
572
  - `0`: All commands executed successfully
@@ -453,6 +580,7 @@ See [versions](./docs/versions.md)
453
580
  - Better UX to indicate what is happening
454
581
  - Tests to avoid regression
455
582
  - Run any shell command rather than assume the command is specified in package.json (? tentative)
583
+ - Promote the advisory `--recommend` phase recommender into an opt-in automatic scheduler that packs each phase under a per-host memory budget at run time
456
584
 
457
585
 
458
586
  ## Disclaimer
package/index.js CHANGED
@@ -51,10 +51,87 @@ const argv = yargs(hideBin(process.argv))
51
51
  type: 'string',
52
52
  description: 'Write HTML report to this path; use "-" for stdout only',
53
53
  })
54
+ .option('render', {
55
+ type: 'string',
56
+ description: 'Render an existing results JSON file to HTML (no run). Use with --html-results.',
57
+ })
58
+ .option('recommend', {
59
+ type: 'string',
60
+ description:
61
+ 'Analyse an existing results JSON and print a memory-aware phase recommendation (R12, no run).',
62
+ })
63
+ .option('fanout', {
64
+ type: 'number',
65
+ description: 'Workspace fan-out (parallel gates sharing the host) used to size the --recommend budget. Default 1.',
66
+ })
67
+ .option('mem-safety', {
68
+ type: 'number',
69
+ description: 'Fraction of total RAM the --recommend budget may use (default 0.8).',
70
+ })
71
+ .option('budget-mb', {
72
+ type: 'number',
73
+ description: 'Override the --recommend memory budget with a fixed value in MB.',
74
+ })
54
75
  .help()
55
76
  .alias('h', 'help')
56
77
  .parse();
57
78
 
79
+ // --render mode: turn an existing results JSON into HTML and exit (no orchestration run).
80
+ // Keeps all HTML rendering in the library so consumers never reimplement it.
81
+ if (argv.render != null) {
82
+ const { renderReportHtml } = await import('./lib/index.js');
83
+ const srcPath = path.resolve(process.cwd(), argv.render);
84
+ if (!fs.existsSync(srcPath)) {
85
+ log.error(`Error: --render source not found at ${srcPath}`);
86
+ process.exit(1);
87
+ }
88
+ let payload;
89
+ try {
90
+ payload = JSON.parse(fs.readFileSync(srcPath, 'utf8'));
91
+ } catch (err) {
92
+ log.error(`Error: failed to parse --render JSON: ${err.message}`);
93
+ process.exit(1);
94
+ }
95
+ const html = renderReportHtml(payload);
96
+ const out = argv.htmlResults ?? null;
97
+ if (out == null || out === '-') {
98
+ console.log(html);
99
+ } else {
100
+ const outPath = path.resolve(process.cwd(), out);
101
+ const tmpPath = outPath + '.tmp';
102
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
103
+ fs.writeFileSync(tmpPath, html, 'utf8');
104
+ fs.renameSync(tmpPath, outPath);
105
+ log.info(`📄 Rendered ${path.relative(process.cwd(), srcPath)} → ${path.relative(process.cwd(), outPath)}`);
106
+ }
107
+ process.exit(0);
108
+ }
109
+
110
+ // --recommend mode: analyse an existing results JSON and print a memory-aware phase
111
+ // recommendation (R12). Advisory only — no orchestration run, no files written.
112
+ if (argv.recommend != null) {
113
+ const { recommendPhases, formatRecommendationReport } = await import('./lib/index.js');
114
+ const srcPath = path.resolve(process.cwd(), argv.recommend);
115
+ if (!fs.existsSync(srcPath)) {
116
+ log.error(`Error: --recommend source not found at ${srcPath}`);
117
+ process.exit(1);
118
+ }
119
+ let payload;
120
+ try {
121
+ payload = JSON.parse(fs.readFileSync(srcPath, 'utf8'));
122
+ } catch (err) {
123
+ log.error(`Error: failed to parse --recommend JSON: ${err.message}`);
124
+ process.exit(1);
125
+ }
126
+ const rec = recommendPhases(payload, {
127
+ fanout: argv.fanout,
128
+ memSafety: argv.memSafety,
129
+ budgetMb: argv.budgetMb,
130
+ });
131
+ console.log(formatRecommendationReport(rec, { sourcePath: path.relative(process.cwd(), srcPath) }));
132
+ process.exit(0);
133
+ }
134
+
58
135
  // Extract arguments
59
136
  const args = argv._;
60
137
  const configPath = args[0] || './scripts-orchestrator.config.js';
@@ -111,6 +188,16 @@ const htmlResultsPath =
111
188
  ? argv.htmlResults
112
189
  : (commandsConfig.html_results ?? commandsConfig.html_results_path ?? null);
113
190
 
191
+ // A7: post-run hook — shell command run after json_results written
192
+ const postRun = commandsConfig.post_run ?? null;
193
+
194
+ // Periodic hook — shell command run on an interval WHILE the run is in flight (e.g. to roll up
195
+ // results into an aggregate report). Library owns only the cadence; the command is project-specific.
196
+ const periodicHook = commandsConfig.periodic_hook ?? null;
197
+ const periodicIntervalMs = Number(commandsConfig.periodic_interval_ms) > 0
198
+ ? Number(commandsConfig.periodic_interval_ms)
199
+ : 45000;
200
+
114
201
  // Set the log folder for the main orchestrator logs if specified
115
202
  if (logFolder) {
116
203
  log.setLogFolder(logFolder);
@@ -128,15 +215,23 @@ const orchestrator = new Orchestrator(
128
215
  jsonResultsPath,
129
216
  htmlResultsPath,
130
217
  );
218
+ // A7: wire post-run hook from config
219
+ orchestrator.postRun = postRun;
220
+ // Wire periodic hook (cadence owned by the library)
221
+ orchestrator.periodicHook = periodicHook;
222
+ orchestrator.periodicIntervalMs = periodicIntervalMs;
131
223
 
132
224
  // Enhanced signal handlers
133
225
  const handleSignal = async (signal) => {
134
226
  log.warn(`\nReceived ${signal} signal. Cleaning up...`);
227
+ orchestrator._stopPeriodicHook();
135
228
  try {
136
229
  await orchestrator.processManager.cleanup();
137
230
  } catch (error) {
138
231
  log.error(`Cleanup failed: ${error.message}`);
139
232
  }
233
+ // A4: clear run-state so dashboards know the run ended
234
+ orchestrator._clearRunState();
140
235
  process.exit(1);
141
236
  };
142
237
 
package/lib/index.js CHANGED
@@ -3,6 +3,28 @@ import { ProcessManager } from './process-manager.js';
3
3
  import { HealthCheck } from './health-check.js';
4
4
  import { Logger } from './logger.js';
5
5
  import { GitCache } from './git-cache.js';
6
+ import { renderReportHtml } from './report-html.js';
7
+ import {
8
+ recommendPhases,
9
+ formatRecommendationReport,
10
+ computeBudget,
11
+ usableSteps,
12
+ observedTimeline,
13
+ packPhases,
14
+ } from './recommend-phases.js';
6
15
 
7
- export { Orchestrator, ProcessManager, HealthCheck, Logger, GitCache };
8
- export default Orchestrator;
16
+ export {
17
+ Orchestrator,
18
+ ProcessManager,
19
+ HealthCheck,
20
+ Logger,
21
+ GitCache,
22
+ renderReportHtml,
23
+ recommendPhases,
24
+ formatRecommendationReport,
25
+ computeBudget,
26
+ usableSteps,
27
+ observedTimeline,
28
+ packPhases,
29
+ };
30
+ export default Orchestrator;