create-pmos 0.48.0 → 0.50.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 (37) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +2 -2
  3. package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
  4. package/payload/mods/pmos-cockpit/README.md +10 -1
  5. package/payload/mods/pmos-cockpit/hooks/lib/route.mjs +95 -0
  6. package/payload/mods/pmos-cockpit/hooks/register.tsx +67 -1
  7. package/payload/mods/pmos-cockpit/types/index.d.ts +1 -1
  8. package/payload/planning/evals/eval-calibration.md +1 -1
  9. package/payload/planning/okrs/health-budgets.md +1 -1
  10. package/payload/scripts/status-facts.js +13 -3
  11. package/payload/scripts/workflow-state.js +7 -2
  12. package/payload/state/discoveries.md +1 -1
  13. package/payload/state/log.md +1 -1
  14. package/workspace/guide/index.md +2 -1
  15. package/workspace/guide/initiatives-and-next-steps.md +3 -1
  16. package/workspace/guide/knowledge-graph.md +12 -1
  17. package/workspace/guide/layout.md +2 -0
  18. package/workspace/guide/live-notes.md +78 -0
  19. package/workspace/guide/validate-with-typesafe.md +15 -1
  20. package/workspace/lib/engine/status-facts.js +13 -3
  21. package/workspace/lib/engine/workflow-state.js +7 -2
  22. package/workspace/lib/jev-categorize.js +47 -13
  23. package/workspace/lib/jev-live.js +37 -0
  24. package/workspace/lib/jev-route.js +11 -1
  25. package/workspace/lib/live-notes.js +171 -26
  26. package/workspace/package-lock.json +1 -1
  27. package/workspace/package.json +8 -6
  28. package/workspace/public/pm/data.js +21 -1
  29. package/workspace/public/pm/modes-live.js +68 -5
  30. package/workspace/public/pm/panel-cockpit.js +3 -1
  31. package/workspace/public/pm/panel-detail.js +3 -1
  32. package/workspace/public/pm/panel-tabs.js +112 -10
  33. package/workspace/public/pm/state.js +3 -0
  34. package/workspace/public/pm/view-livenotes.js +397 -0
  35. package/workspace/run-tests.js +148 -0
  36. package/workspace/server.js +71 -9
  37. package/workspace/test-files.json +180 -0
@@ -76,6 +76,7 @@ const jevClaims = require('./lib/jev-claims.js'); // D80 — the bridge betwee
76
76
  const jevCategorize = require('./lib/jev-categorize.js'); // D82 — the bridge between the document categorizer and the one TypeSafe module
77
77
  const validateLib = require('./lib/validate.js');
78
78
  const liveNotes = require('./lib/live-notes.js'); // jev-live-notes — reads and appends a repository's live-notes ledger; no network
79
+ const jevLive = require('./lib/jev-live.js'); // jev-live-notes Run 7 (D85) — the live view's one bridge to Jev, behind three gates
79
80
  const claimsLib = require('./lib/claims.js');
80
81
  const docCategory = require('./lib/doc-category.js'); // jev-doc-categorizer Run 1 — the PM-facing kind of a document, derived, local, no model
81
82
  const docCatCorrections = require('./lib/doc-category-corrections.js'); // jev-doc-categorizer Run 3 — the PM's own category, per user, outside the repository
@@ -291,6 +292,10 @@ function sessionEnv() {
291
292
  // print its environment, so the key is never handed to one, whether it came from settings or the shell.
292
293
  delete env[tsSettings.ENV_NAME];
293
294
  if (SET_GIT_OPTIONAL_LOCKS) delete env.GIT_OPTIONAL_LOCKS;
295
+ // prompt-model-routing Run 3 — where the cockpit mod asks for a prompt's route: this server, on loopback.
296
+ // An address only; the Jev key stays here (D79).
297
+ const routeUrl = jevRoute.routeUrl(PORT);
298
+ if (routeUrl) env.PMOS_ROUTE_URL = routeUrl; else delete env.PMOS_ROUTE_URL;
294
299
  return env;
295
300
  }
296
301
 
@@ -470,6 +475,12 @@ function homeWorkspace() {
470
475
  return HOME_ENABLED ? workspaces.get(wsId(SELF_ROOT)) : [...workspaces.values()][0];
471
476
  }
472
477
  function getWs(id) { return (id && workspaces.get(id)) || homeWorkspace(); }
478
+ /** The registered workspace whose root is this path exactly, or null. Never the home workspace by default. */
479
+ function wsByRoot(root) {
480
+ const want = path.resolve(String(root));
481
+ for (const w of workspaces.values()) if (w && path.resolve(w.root) === want) return w;
482
+ return null;
483
+ }
473
484
  // R2/R3 — what the sidebar tree needs and cannot derive for itself: which REPOSITORY a registered
474
485
  // working tree belongs to, and which branch it is on. `git worktree list` names the main worktree
475
486
  // first from any of a repo's trees (git-info.worktreesAt), so index 0 is the group key: it is what
@@ -1608,8 +1619,8 @@ function annotateJev(root, node, cache, seen = new Set()) {
1608
1619
  if (node.category === docCategory.UNDETERMINED && typeof node.path === 'string' && /\.md$/i.test(node.path)) {
1609
1620
  const c = categoryInput(path.join(root, node.path));
1610
1621
  const l = c ? jevCategorize.lookup(cache, { fm: c.fm, text: c.text }) : null;
1611
- if (l && l.decided) { node.category = l.category; node.category_basis = 'jev'; node.category_why = l.sentence; if (l.ticket_kind) node.ticket_kind = l.ticket_kind; }
1612
- else if (l) node.category_why = l.sentence;
1622
+ if (l && l.decided) { node.category = l.category; node.category_basis = 'jev'; node.category_why = l.sentence; if (l.ticket_kind) node.category_kind = l.ticket_kind; }
1623
+ else if (l) { node.category_why = l.sentence; node.category_jev = 'unsure'; }
1613
1624
  }
1614
1625
  for (const k of Object.keys(node)) if (node[k] && typeof node[k] === 'object') annotateJev(root, node[k], cache, seen);
1615
1626
  return node;
@@ -2285,8 +2296,11 @@ function checkDueSchedules() {
2285
2296
  } catch {}
2286
2297
  return fired;
2287
2298
  }
2288
- // the tick — the persistent server checks on an interval; unref'd so it never blocks exit
2289
- try { const _tick = setInterval(() => { checkDueSchedules(); proactiveTick(); try { prepareTick({ manual: false }); } catch { /* the next tick tries again */ } }, 60000); if (_tick.unref) _tick.unref(); } catch {}
2299
+ // the tick — the persistent server checks on an interval; unref'd so it never blocks exit.
2300
+ // PMOS_TICK_MS exists for the suites (same footing as PMOS_LIVENESS_MS): proactive-prepare.test.js
2301
+ // used to wait on two real minutes of this clock. Unset, the interval is the 60 seconds it always was.
2302
+ const TICK_MS = Math.max(200, Number(process.env.PMOS_TICK_MS) || 60000);
2303
+ try { const _tick = setInterval(() => { checkDueSchedules(); proactiveTick(); try { prepareTick({ manual: false }); } catch { /* the next tick tries again */ } }, TICK_MS); if (_tick.unref) _tick.unref(); } catch {}
2290
2304
 
2291
2305
  // ── Sessions: real PTYs, owned by the server, with disk-backed scrollback + metadata. ──
2292
2306
  /** @type {Map<string, {id,ws,name,agent,cwd,state,note,created,pty,buf,cols,rows,clients:Set}>} */
@@ -4703,6 +4717,18 @@ function jevGateInfo(w) {
4703
4717
  const { cacheDir } = claimsPaths(w);
4704
4718
  return { switch: f.jev, status_read: f.status_read, open: g.open, missing: g.missing, sentence: g.open ? null : jevClaims.gateSentence(g.missing), ...jevClaims.status(cacheDir) };
4705
4719
  }
4720
+ // D85 — the live-notes add route's sender: lib/typesafe.js behind the key, consent and the switch, or none. The key goes to the engine
4721
+ // only when the sender does, and never into a response.
4722
+ function liveSender(w) {
4723
+ const key = tsSettings.getKey(STATE);
4724
+ const flags = { consent: tsSettings.getRepo(STATE, w.root).consent, live: liveNotes.getSwitch(STATE, w.root).live };
4725
+ const snd = jevLive.senderFor(key, flags, jevModelOpts(w.root));
4726
+ return { key: snd.ask ? key : null, flags, ask: snd.ask, askOpts: snd.askOpts };
4727
+ }
4728
+ // D85 — what the live-notes view needs to explain its Jev switch. Booleans and sentences only; the key never leaves the store.
4729
+ function liveJevInfo(w) {
4730
+ return jevLive.infoFor(tsSettings.status(STATE).present, { consent: tsSettings.getRepo(STATE, w.root).consent, live: liveNotes.getSwitch(STATE, w.root).live });
4731
+ }
4706
4732
  // D82 — what the page needs to explain the categorize switch: which gates hold, and what the last pass did.
4707
4733
  const doccatRunning = new Set();
4708
4734
  const doccatAutoAt = new Map(); // root → time of the last automatic pass; at most one a minute
@@ -5555,11 +5581,13 @@ async function handleRequest(req, res) {
5555
5581
  }
5556
5582
  if (p === '/api/model/route' && req.method === 'POST') {
5557
5583
  let body = {}; try { body = JSON.parse((await readBody(req)) || '{}'); } catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
5558
- const w = getWs(body.ws); if (!w) return json(res, { ok: false, error: 'no workspace' }, 400);
5584
+ // A terminal's cockpit mod names its repository by root; the page names a workspace by id.
5585
+ const w = typeof body.root === 'string' ? wsByRoot(body.root) : getWs(body.ws);
5586
+ if (!w) return json(res, { ok: false, error: 'no workspace for that repository' }, 400);
5559
5587
  if (typeof body.prompt !== 'string' || !body.prompt.trim() || body.prompt.length > 60000) return json(res, { ok: false, error: 'prompt must be text of up to 60000 characters' }, 400);
5560
5588
  const f = tsSettings.getRepo(STATE, w.root);
5561
5589
  const out = await jevRoute.routePrompt({ root: w.root, prompt: body.prompt, ctx: { skill: typeof body.skill === 'string' ? body.skill : undefined, source: body.source, running: true },
5562
- key: tsSettings.getKey(STATE), flags: { consent: f.consent, route: f.route, exclude: f.exclude } });
5590
+ key: tsSettings.getKey(STATE), flags: { consent: f.consent, route: f.route, exclude: f.exclude }, askOpts: body.interactive === true ? jevRoute.INTERACTIVE : {} });
5563
5591
  if (!out.ok) return json(res, { ok: false, error: out.error }, 400);
5564
5592
  if (out.shadow) return json(res, { ok: true, shadow: true, id: out.id }); // shadow: logged, nothing shown, nothing applied
5565
5593
  return json(res, { ok: true, ws: w.id, id: out.id, tier: out.tier, model: out.model, effort: out.effort, stage: out.stage, confidence: out.confidence, why: out.why,
@@ -6566,12 +6594,14 @@ async function handleRequest(req, res) {
6566
6594
  return json(res, { ok: true, ws: w.id, name: w.name, ...tsSettings.getRepo(STATE, w.root), key: tsSettings.status(STATE) });
6567
6595
  }
6568
6596
  /* ── live notes (jev-live-notes, Run 2): the ranked proposals of a conversation, and the PM's accept, change or reject.
6569
- * Reads the repository's ledger; the one write is a decision row. No request leaves this process, and no page is built yet. ── */
6597
+ * Reads the repository's ledger; the writes are ledger rows and, on close, the day's note. No request leaves this process.
6598
+ * The page is the cockpit's live-notes view (public/pm/view-livenotes.js, Run 6). ── */
6570
6599
  if (p === '/api/live-notes' && req.method === 'GET') {
6571
6600
  const wsq = url.searchParams.get('ws');
6572
6601
  const w = wsq ? workspaces.get(wsq) : homeWorkspace(); // a named workspace the server does not know is refused, never replaced by the home one
6573
6602
  if (!w) return json(res, { ok: false, error: 'no such workspace' }, 400);
6574
- const r = liveNotes.apiList({ root: w.root, session: url.searchParams.get('session') || null });
6603
+ const r = liveNotes.apiList({ root: w.root, session: url.searchParams.get('session') || null, exclude: tsSettings.getRepo(STATE, w.root).exclude });
6604
+ if (r.ok) r.jev = liveJevInfo(w);
6575
6605
  return json(res, r, r.ok ? 200 : (r.status || 400));
6576
6606
  }
6577
6607
  if (p === '/api/live-notes/decide' && req.method === 'POST') {
@@ -6580,7 +6610,39 @@ async function handleRequest(req, res) {
6580
6610
  catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
6581
6611
  const w = body.ws ? workspaces.get(String(body.ws)) : homeWorkspace();
6582
6612
  if (!w) return json(res, { ok: false, error: 'no such workspace' }, 400);
6583
- const r = liveNotes.apiDecide({ root: w.root, runtimeDir: STATE, session: body.session, segment: body.segment, action: body.action, initiative: body.initiative || null, relation: body.relation || null, exclude: tsSettings.getRepo(STATE, w.root).exclude });
6613
+ const r = liveNotes.apiDecide({ root: w.root, runtimeDir: STATE, session: body.session, segment: body.segment, action: body.action, initiative: body.initiative || null, relation: body.relation || null, question: body.question == null ? null : body.question, exclude: tsSettings.getRepo(STATE, w.root).exclude });
6614
+ return json(res, r, r.ok ? 200 : (r.status || 400));
6615
+ }
6616
+ /* Run 7 (D85): the per-repository switch "live notes may use Jev". Default off; it is one of three gates, with the key and consent. */
6617
+ if (p === '/api/live-notes/switch' && req.method === 'POST') {
6618
+ let body = {};
6619
+ try { body = JSON.parse((await readBody(req)) || '{}'); }
6620
+ catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
6621
+ const w = body.ws ? workspaces.get(String(body.ws)) : homeWorkspace();
6622
+ if (!w) return json(res, { ok: false, error: 'no such workspace' }, 400);
6623
+ if (typeof body.live !== 'boolean') return json(res, { ok: false, error: 'say live: true or live: false' }, 400);
6624
+ liveNotes.setSwitch(STATE, w.root, { live: body.live });
6625
+ return json(res, { ok: true, ws: w.id, jev: liveJevInfo(w) });
6626
+ }
6627
+ /* Run 6: the live-notes view's writes — start or resume a session, add points, close. A proposal is a local word match made in
6628
+ * this process, unless all three D85 gates hold: then `add` hands each cleaned point to Jev through lib/jev-live.js. */
6629
+ if ((p === '/api/live-notes/session' || p === '/api/live-notes/add' || p === '/api/live-notes/close' || p === '/api/live-notes/private') && req.method === 'POST') {
6630
+ let body = {};
6631
+ try { body = JSON.parse((await readBody(req)) || '{}'); }
6632
+ catch { return json(res, { ok: false, error: 'invalid JSON body' }, 400); }
6633
+ const w = body.ws ? workspaces.get(String(body.ws)) : homeWorkspace();
6634
+ if (!w) return json(res, { ok: false, error: 'no such workspace' }, 400);
6635
+ const exclude = tsSettings.getRepo(STATE, w.root).exclude;
6636
+ let r;
6637
+ if (p === '/api/live-notes/session') r = liveNotes.apiOpen({ root: w.root, runtimeDir: STATE, session: body.session || null, anchor: body.anchor || null, exclude });
6638
+ else if (p === '/api/live-notes/add') {
6639
+ const snd = liveSender(w);
6640
+ r = await liveNotes.apiAdd({ root: w.root, runtimeDir: STATE, session: body.session, text: body.text, source: body.source === 'speech' ? 'speech' : 'typed', exclude,
6641
+ key: snd.key, flags: snd.flags, ask: snd.ask, askOpts: snd.askOpts });
6642
+ }
6643
+ else if (p === '/api/live-notes/private') r = liveNotes.apiMarkPrivate({ root: w.root, session: body.session });
6644
+ else r = liveNotes.apiClose({ root: w.root, runtimeDir: STATE, session: body.session, retentionDays: liveNotes.getSwitch(STATE, w.root).retention_days, exclude });
6645
+ if (r.ok && p !== '/api/live-notes/close') { r.jev = liveJevInfo(w); if (r.note) r.note = r.jev.note; }
6584
6646
  return json(res, r, r.ok ? 200 : (r.status || 400));
6585
6647
  }
6586
6648
  if (p === '/api/validate/plan' && req.method === 'POST') {
@@ -0,0 +1,180 @@
1
+ {
2
+ "_comment": "The test files each npm test script runs. run-tests.js reads this file; add a new suite here and every runner, local and CI, picks it up. Every suite claims its own port, state directory and tmux socket (test-run.js or its own equivalent), which is what makes running them side by side safe.",
3
+ "_comment_exclusive": "Suites that write the checkout's own state (workspace/state/, the home workspace's ledgers) while they run. run-tests.js runs each of them with nothing else beside it, because suites that read the checkout as their home workspace (proactive.test.js's scan, for one) would see those writes mid-run.",
4
+ "exclusive": [
5
+ "test.js"
6
+ ],
7
+ "unit": [
8
+ "test.js"
9
+ ],
10
+ "modules": [
11
+ "proactive.test.js",
12
+ "focus.test.js",
13
+ "handoff.test.js",
14
+ "repo-scan.test.js",
15
+ "profiles.test.js",
16
+ "external-linking.test.js",
17
+ "story-artifacts.test.js",
18
+ "no-network.test.js",
19
+ "typesafe-validation.test.js",
20
+ "claims.test.js",
21
+ "status-facts-panel.test.js",
22
+ "jev-claims.test.js",
23
+ "jev-categorize.test.js",
24
+ "jev-status.test.js",
25
+ "jev-route.test.js",
26
+ "repo-watch.test.js",
27
+ "fixture-parity.test.mjs",
28
+ "markdown.test.mjs",
29
+ "start-policy.test.js",
30
+ "state-isolation.test.js",
31
+ "sync.test.js",
32
+ "schedule-cli.test.js",
33
+ "initiative-surface.test.js",
34
+ "guide.test.js",
35
+ "next-step-actions.test.js",
36
+ "kg.test.js",
37
+ "doc-category.test.js",
38
+ "doc-category-corrections.test.js",
39
+ "proactive-prepare.test.js",
40
+ "learn.test.js",
41
+ "session-flag.test.js",
42
+ "live-notes.test.js"
43
+ ],
44
+ "panels": [
45
+ "pm-shell-test.js",
46
+ "pm-repos-test.js",
47
+ "pm-initiatives-test.js",
48
+ "pm-detail-test.js",
49
+ "pm-tabs-test.js",
50
+ "pm-term-test.js",
51
+ "pm-modes-test.js",
52
+ "pm-r1-test.js",
53
+ "pm-r2-test.js",
54
+ "pm-r3-test.js",
55
+ "pm-r4-test.js",
56
+ "pm-r5-test.js",
57
+ "pm-r6-test.js",
58
+ "pm-r7-test.js",
59
+ "pm-r8-test.js",
60
+ "pm-r9-test.js",
61
+ "pm-r10-test.js",
62
+ "pm-r11-test.js",
63
+ "pm-r12-test.js",
64
+ "pm-r13-test.js",
65
+ "pm-r14-test.js",
66
+ "pm-r15-test.js",
67
+ "pm-r16-test.js",
68
+ "pm-r17-test.js",
69
+ "pm-r18-test.js",
70
+ "pm-r19-test.js",
71
+ "pm-r20-test.js",
72
+ "pm-r21-test.js",
73
+ "pm-r22-test.js",
74
+ "pm-r23-test.js",
75
+ "pm-r24-test.js",
76
+ "pm-r25-test.js",
77
+ "pm-r26-test.js",
78
+ "pm-doc-category-test.js",
79
+ "pm-r27-test.js",
80
+ "pm-r28-test.js",
81
+ "pm-r29-today-test.js",
82
+ "pm-r29-portfolio-test.js",
83
+ "pm-r29-knowledge-test.js",
84
+ "pm-r29-memory-test.js",
85
+ "pm-r29-skills-scheduled-test.js",
86
+ "pm-r29-validate-guide-test.js",
87
+ "pm-r29-reports-addrepo-test.js",
88
+ "pm-r29-filespad-test.js",
89
+ "pm-r29-newsession-alignment-test.js",
90
+ "pm-live-notes-test.js",
91
+ "pm-r30-test.js"
92
+ ],
93
+ "ui": [
94
+ "ui-test.js"
95
+ ],
96
+ "seconds": {
97
+ "test.js": 48,
98
+ "proactive.test.js": 4,
99
+ "focus.test.js": 1,
100
+ "handoff.test.js": 1,
101
+ "repo-scan.test.js": 1,
102
+ "profiles.test.js": 1,
103
+ "external-linking.test.js": 1,
104
+ "story-artifacts.test.js": 1,
105
+ "no-network.test.js": 1,
106
+ "typesafe-validation.test.js": 1,
107
+ "claims.test.js": 1,
108
+ "status-facts-panel.test.js": 1,
109
+ "jev-claims.test.js": 3,
110
+ "jev-categorize.test.js": 1,
111
+ "jev-status.test.js": 1,
112
+ "jev-route.test.js": 1,
113
+ "repo-watch.test.js": 1,
114
+ "fixture-parity.test.mjs": 1,
115
+ "markdown.test.mjs": 1,
116
+ "start-policy.test.js": 1,
117
+ "state-isolation.test.js": 4,
118
+ "sync.test.js": 4,
119
+ "schedule-cli.test.js": 3,
120
+ "initiative-surface.test.js": 3,
121
+ "guide.test.js": 1,
122
+ "next-step-actions.test.js": 5,
123
+ "kg.test.js": 1,
124
+ "doc-category.test.js": 1,
125
+ "doc-category-corrections.test.js": 1,
126
+ "proactive-prepare.test.js": 9,
127
+ "learn.test.js": 3,
128
+ "session-flag.test.js": 1,
129
+ "live-notes.test.js": 3,
130
+ "pm-shell-test.js": 6,
131
+ "pm-repos-test.js": 3,
132
+ "pm-initiatives-test.js": 15,
133
+ "pm-detail-test.js": 16,
134
+ "pm-tabs-test.js": 59,
135
+ "pm-term-test.js": 28,
136
+ "pm-modes-test.js": 18,
137
+ "pm-r1-test.js": 6,
138
+ "pm-r2-test.js": 3,
139
+ "pm-r3-test.js": 3,
140
+ "pm-r4-test.js": 4,
141
+ "pm-r5-test.js": 3,
142
+ "pm-r6-test.js": 2,
143
+ "pm-r7-test.js": 1,
144
+ "pm-r8-test.js": 1,
145
+ "pm-r9-test.js": 2,
146
+ "pm-r10-test.js": 1,
147
+ "pm-r11-test.js": 1,
148
+ "pm-r12-test.js": 6,
149
+ "pm-r13-test.js": 2,
150
+ "pm-r14-test.js": 7,
151
+ "pm-r15-test.js": 35,
152
+ "pm-r16-test.js": 7,
153
+ "pm-r17-test.js": 3,
154
+ "pm-r18-test.js": 4,
155
+ "pm-r19-test.js": 18,
156
+ "pm-r20-test.js": 4,
157
+ "pm-r21-test.js": 4,
158
+ "pm-r22-test.js": 32,
159
+ "pm-r23-test.js": 13,
160
+ "pm-r24-test.js": 4,
161
+ "pm-r25-test.js": 9,
162
+ "pm-r26-test.js": 9,
163
+ "pm-doc-category-test.js": 4,
164
+ "pm-r27-test.js": 26,
165
+ "pm-r28-test.js": 9,
166
+ "pm-r29-today-test.js": 4,
167
+ "pm-r29-portfolio-test.js": 3,
168
+ "pm-r29-knowledge-test.js": 3,
169
+ "pm-r29-memory-test.js": 4,
170
+ "pm-r29-skills-scheduled-test.js": 6,
171
+ "pm-r29-validate-guide-test.js": 20,
172
+ "pm-r29-reports-addrepo-test.js": 4,
173
+ "pm-r29-filespad-test.js": 5,
174
+ "pm-r29-newsession-alignment-test.js": 8,
175
+ "pm-live-notes-test.js": 24,
176
+ "pm-r30-test.js": 4,
177
+ "ui-test.js": 30
178
+ },
179
+ "_comment_seconds": "Measured wall time of each suite run alone (4-CPU sandbox, 2026-10-05). Only the ORDER uses it: the slowest suites start first, and --shard deals suites to the least-loaded shard so the shards finish together. A wrong or missing number never skips a suite; a missing one counts as 10."
180
+ }