session-orchestrator 3.17.0 → 3.20.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/030-wave-execution.mdc +17 -1
- package/CHANGELOG.md +185 -412
- package/README.md +12 -9
- package/SECURITY.md +190 -27
- package/agents/AGENTS.md +20 -3
- package/agents/code-implementer.md +6 -6
- package/agents/db-specialist.md +1 -1
- package/agents/qa-strategist.md +31 -6
- package/agents/schemas/qa-strategist.schema.json +27 -0
- package/agents/schemas/test-writer.schema.json +60 -2
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +1 -1
- package/agents/test-writer.md +29 -10
- package/agents/ui-developer.md +1 -1
- package/commands/contract-version-bump.md +28 -0
- package/commands/portfolio.md +1 -1
- package/commands/session.md +6 -2
- package/docs/USER-GUIDE.md +9 -4
- package/docs/ci-setup.md +121 -7
- package/docs/codex-setup.md +1 -1
- package/docs/components.md +6 -6
- package/docs/cursor-setup.md +22 -9
- package/docs/events-schema.md +5 -1
- package/docs/instruction-delivery.md +794 -0
- package/docs/rule-authoring.md +58 -9
- package/docs/session-config-reference.md +245 -50
- package/docs/session-config-template.md +39 -26
- package/hooks/_lib/guard-source-loader.mjs +680 -0
- package/hooks/_lib/lock-bootstrap.mjs +21 -0
- package/hooks/_lib/vcs-create-matcher.mjs +119 -0
- package/hooks/config-protection.mjs +0 -0
- package/hooks/enforce-commands.mjs +226 -19
- package/hooks/enforce-scope.mjs +133 -9
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +11 -2
- package/hooks/hooks-pi.json +10 -0
- package/hooks/hooks.json +21 -1
- package/hooks/on-session-end.mjs +178 -18
- package/hooks/on-session-start.mjs +30 -4
- package/hooks/post-bash-write-verify.mjs +977 -0
- package/hooks/post-subagent-discovery-validator.mjs +256 -41
- package/hooks/pre-bash-destructive-guard.mjs +616 -164
- package/hooks/pre-bash-issue-budget.mjs +167 -0
- package/hooks/pre-bash-sessions-ledger-guard.mjs +1054 -0
- package/hooks/pre-bash-templates-first.mjs +96 -63
- package/hooks/subagent-telemetry.mjs +527 -37
- package/package.json +6 -3
- package/pi/prompts/contract-version-bump.md +12 -0
- package/rules/README.md +32 -0
- package/scripts/archive-closed-prds.mjs +12 -22
- package/scripts/autopilot-multi.mjs +103 -20
- package/scripts/backfill-abandoned-sessions.mjs +160 -4
- package/scripts/backfill-learnings-from-vault.mjs +967 -0
- package/scripts/check-doc-consistency.sh +17 -1
- package/scripts/emit-session.mjs +3 -40
- package/scripts/eval-session.mjs +50 -9
- package/scripts/fleet-instruction-scan.mjs +141 -0
- package/scripts/lib/autopilot/mr-draft.mjs +31 -1
- package/scripts/lib/autopilot/worktree-pipeline.mjs +113 -5
- package/scripts/lib/backlog-scan.mjs +39 -6
- package/scripts/lib/blocked-commands-policy.mjs +340 -0
- package/scripts/lib/ci-status-banner.mjs +75 -12
- package/scripts/lib/claude-md-budget-lint.mjs +283 -34
- package/scripts/lib/command-blocker.mjs +1273 -58
- package/scripts/lib/config/config-protection.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +9 -1
- package/scripts/lib/config/gitlab-portfolio.mjs +1 -1
- package/scripts/lib/config/issue-budget.mjs +123 -0
- package/scripts/lib/config/reconcile.mjs +21 -0
- package/scripts/lib/config/section-extractor.mjs +121 -1
- package/scripts/lib/config-schema.mjs +23 -3
- package/scripts/lib/config.mjs +17 -0
- package/scripts/lib/convergence-monitor.mjs +49 -3
- package/scripts/lib/description-surface.mjs +535 -0
- package/scripts/lib/dispatcher/enumerate.mjs +26 -40
- package/scripts/lib/ecosystem-wizard/config-writer.mjs +26 -24
- package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +1 -1
- package/scripts/lib/eval/engine.mjs +47 -5
- package/scripts/lib/events.mjs +59 -7
- package/scripts/lib/gates/gate-full.mjs +15 -3
- package/scripts/lib/gates/gate-helpers.mjs +132 -6
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +28 -8
- package/scripts/lib/gitlab-portfolio/aggregator.mjs +8 -2
- package/scripts/lib/gitlab-portfolio/cli.mjs +1 -1
- package/scripts/lib/handover-gate.mjs +7 -3
- package/scripts/lib/hardening.mjs +9 -9
- package/scripts/lib/harness-audit/categories/category4.mjs +9 -3
- package/scripts/lib/instruction-budget-guard.mjs +402 -51
- package/scripts/lib/io.mjs +345 -10
- package/scripts/lib/issue-budget.mjs +269 -0
- package/scripts/lib/issue-close-strip-labels.mjs +39 -9
- package/scripts/lib/label-scope.mjs +47 -0
- package/scripts/lib/learnings/affinity.mjs +434 -0
- package/scripts/lib/learnings/candidates.mjs +736 -0
- package/scripts/lib/learnings/expiry-sweep.mjs +408 -53
- package/scripts/lib/learnings/judgment.mjs +782 -0
- package/scripts/lib/learnings/kebab.mjs +128 -0
- package/scripts/lib/learnings/schema.mjs +43 -3
- package/scripts/lib/learnings/select.mjs +550 -0
- package/scripts/lib/lock-reaper.mjs +1 -2
- package/scripts/lib/memory-proposals/schema.mjs +36 -1
- package/scripts/lib/peer-discovery.mjs +645 -0
- package/scripts/lib/pi-hook-bridge.mjs +146 -17
- package/scripts/lib/product-repo-detect.mjs +9 -8
- package/scripts/lib/project-hygiene.mjs +432 -0
- package/scripts/lib/quality-gate.mjs +167 -0
- package/scripts/lib/recommendations-v0.mjs +1 -1
- package/scripts/lib/reconcile/eligibility.mjs +1 -1
- package/scripts/lib/reconcile/emitter.mjs +128 -24
- package/scripts/lib/reconcile/engine.mjs +156 -54
- package/scripts/lib/reconcile/idempotency.mjs +114 -14
- package/scripts/lib/reconcile/renderer.mjs +141 -25
- package/scripts/lib/reconcile/sanitize.mjs +518 -0
- package/scripts/lib/reconcile/writer.mjs +95 -1
- package/scripts/lib/reconcile-nudge-banner.mjs +65 -9
- package/scripts/lib/resource-probe/evaluate.mjs +70 -4
- package/scripts/lib/resource-probe.mjs +19 -0
- package/scripts/lib/rule-loader.mjs +6 -0
- package/scripts/lib/scope-baseline.mjs +564 -0
- package/scripts/lib/scope-gate.mjs +568 -145
- package/scripts/lib/session-close-backfill.mjs +63 -8
- package/scripts/lib/session-end/phase-skip.mjs +1 -0
- package/scripts/lib/session-id.mjs +221 -41
- package/scripts/lib/session-lock.mjs +304 -6
- package/scripts/lib/session-record-repair.mjs +551 -0
- package/scripts/lib/session-schema/constants.mjs +22 -3
- package/scripts/lib/session-schema/serializer.mjs +54 -0
- package/scripts/lib/session-schema/validator.mjs +16 -0
- package/scripts/lib/session-schema.mjs +1 -0
- package/scripts/lib/session-token-rollup.mjs +68 -6
- package/scripts/lib/sessions-integrity-banner.mjs +294 -0
- package/scripts/lib/sessions-staleness-banner.mjs +121 -12
- package/scripts/lib/skill-evolution/idempotency.mjs +135 -16
- package/scripts/lib/skill-evolution/mr-opener.mjs +9 -1
- package/scripts/lib/soul-resolve.mjs +12 -0
- package/scripts/lib/spiral-carryover.mjs +142 -30
- package/scripts/lib/state-md/mission-status.mjs +53 -3
- package/scripts/lib/subagents-schema.mjs +43 -9
- package/scripts/lib/test-runner/issue-reconcile.mjs +53 -13
- package/scripts/lib/tests-src-ratio.mjs +484 -0
- package/scripts/lib/tmux-layout/telemetry.mjs +43 -10
- package/scripts/lib/validate/check-agents.mjs +56 -0
- package/scripts/lib/validate/check-banner-parity.mjs +376 -0
- package/scripts/lib/validate/check-guard-requires-parity.mjs +1148 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
- package/scripts/lib/validate/check-learning-provenance.mjs +511 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +3 -3
- package/scripts/lib/validate/check-rules.mjs +244 -36
- package/scripts/lib/validate/check-test-value-bans.mjs +782 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-unwired-features.mjs +549 -0
- package/scripts/lib/validate-vendored-rules.mjs +10 -2
- package/scripts/lib/vault-archive.mjs +17 -2
- package/scripts/lib/vault-backfill/glab.mjs +8 -0
- package/scripts/lib/vault-mirror/process.mjs +30 -0
- package/scripts/lib/vault-mirror/render-sessions.mjs +293 -36
- package/scripts/lib/vcs-repo-spec.mjs +362 -0
- package/scripts/lib/wave-resource-gate.mjs +115 -11
- package/scripts/lib/worktree/listing.mjs +44 -7
- package/scripts/mcp-server.sh +17 -3
- package/scripts/measure-context-overhead.sh +151 -0
- package/scripts/memory-propose.mjs +72 -9
- package/scripts/print-applicable-rules.mjs +218 -16
- package/scripts/print-learnings-index.mjs +474 -0
- package/scripts/release.mjs +534 -0
- package/scripts/repair-invalid-sessions.mjs +209 -0
- package/scripts/run-quality-gate.mjs +123 -5
- package/scripts/sweep-expired-learnings.mjs +192 -32
- package/scripts/validate-plugin.mjs +21 -0
- package/scripts/validate-wave-scope.mjs +182 -17
- package/scripts/vault-integration-watcher.mjs +32 -10
- package/skills/_shared/config-reading.md +2 -2
- package/skills/bootstrap/fast-template.md +1 -1
- package/skills/brainstorm/soul.md +47 -1
- package/skills/claude-md-drift-check/checker.mjs +145 -28
- package/skills/contract-version-bump/SKILL.md +219 -0
- package/skills/discovery/SKILL.md +4 -4
- package/skills/discovery/issue-templates.md +11 -11
- package/skills/discovery/probes-audit.md +1 -1
- package/skills/discovery/probes-feature.md +1 -1
- package/skills/discovery/probes-session.md +26 -5
- package/skills/ecosystem-health/SKILL.md +1 -1
- package/skills/ecosystem-health/wizard.md +4 -4
- package/skills/evolve/SKILL.md +117 -18
- package/skills/gitlab-ops/SKILL.md +25 -12
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- package/skills/grill/soul.md +44 -1
- package/skills/hook-development/SKILL.md +1 -1
- package/skills/mode-selector/SKILL.md +1 -1
- package/skills/npm-publish/SKILL.md +17 -1
- package/skills/plan/SKILL.md +5 -5
- package/skills/plan/mode-feature.md +4 -4
- package/skills/plan/mode-new.md +10 -10
- package/skills/plan/mode-retro.md +1 -1
- package/skills/plan/soul.md +46 -3
- package/skills/quality-gates/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +21 -4
- package/skills/session-end/SKILL.md +34 -36
- package/skills/session-end/discovery-scan.md +4 -2
- package/skills/session-end/drift-operations.md +4 -4
- package/skills/session-end/metrics-collection.md +13 -0
- package/skills/session-end/phase-3-2-docs-verification.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +32 -2
- package/skills/session-end/plan-verification.md +6 -7
- package/skills/session-end/session-metrics-write.md +2 -0
- package/skills/session-end/vault-operations.md +1 -1
- package/skills/session-end/verification-checklist.md +1 -1
- package/skills/session-plan/SKILL.md +6 -2
- package/skills/session-plan/wave-template.md +2 -0
- package/skills/session-start/SKILL.md +75 -7
- package/skills/session-start/phase-4-5-resource-health.md +15 -2
- package/skills/session-start/soul.md +41 -1
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +108 -7
- package/skills/wave-executor/SKILL.md +6 -7
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/wave-loop.md +198 -80
- package/templates/_shared/loop.md +4 -4
|
@@ -23,9 +23,13 @@
|
|
|
23
23
|
* `.orchestrator/runtime/reconcile-candidates.jsonl` (owned by
|
|
24
24
|
* `scripts/lib/reconcile/idempotency.mjs`). Every learning present at a given
|
|
25
25
|
* run (eligible OR rejected) gets a candidate row, so:
|
|
26
|
-
* - the MAX `created_at` across `loadCandidates({repoRoot})` is the
|
|
27
|
-
* recent reconcile run's timestamp (or `null` when the store is
|
|
28
|
-
* i.e. no run on record, matching the fleet finding above).
|
|
26
|
+
* - the MAX `created_at` across `loadCandidates({repoRoot}).records` is the
|
|
27
|
+
* most recent reconcile run's timestamp (or `null` when the store is
|
|
28
|
+
* empty — i.e. no run on record, matching the fleet finding above).
|
|
29
|
+
* `.skipped` is read alongside it because `records.length === 0` alone is
|
|
30
|
+
* AMBIGUOUS: a missing store and a store whose every line failed the
|
|
31
|
+
* candidate shape guard both yield `[]`. Reporting "never" for the latter
|
|
32
|
+
* denies a run whose record was merely quarantined (GitLab #955 finding 2).
|
|
29
33
|
* - the candidate COUNT is a reasonable proxy for "how many learnings had
|
|
30
34
|
* been seen as of the last run" (dedup is by `learning_key`, so it is a
|
|
31
35
|
* high-water mark across all runs to date), used for the "new learnings
|
|
@@ -116,7 +120,11 @@ function _lastRunAt(candidates) {
|
|
|
116
120
|
* delta: number,
|
|
117
121
|
* nudge: boolean,
|
|
118
122
|
* reasons: string[],
|
|
123
|
+
* skippedCandidates?: number,
|
|
119
124
|
* }>}
|
|
125
|
+
* `skippedCandidates` is ABSENT when the candidate store was never inspected
|
|
126
|
+
* (early empty-corpus return, or an unreadable store); `0` means inspected and
|
|
127
|
+
* clean, `> 0` means that many persisted lines failed the candidate shape guard.
|
|
120
128
|
*/
|
|
121
129
|
export async function computeReconcileNudge(opts = {}) {
|
|
122
130
|
const empty = {
|
|
@@ -167,20 +175,39 @@ export async function computeReconcileNudge(opts = {}) {
|
|
|
167
175
|
|
|
168
176
|
/** @type {Array<Record<string, unknown>>} */
|
|
169
177
|
let candidates;
|
|
178
|
+
/** @type {number|undefined} */
|
|
179
|
+
let skippedCandidates;
|
|
170
180
|
try {
|
|
171
|
-
|
|
181
|
+
const diag = loadCandidates({ repoRoot });
|
|
182
|
+
candidates = Array.isArray(diag?.records) ? diag.records : [];
|
|
183
|
+
// Absence-preserving, mirroring `engine.mjs` `summary.skipped`: only a
|
|
184
|
+
// finite count means "the store was inspected". Absent ⇒ never checked,
|
|
185
|
+
// 0 ⇒ checked and clean.
|
|
186
|
+
if (Number.isFinite(diag?.skipped)) skippedCandidates = Number(diag.skipped);
|
|
172
187
|
} catch {
|
|
173
188
|
candidates = [];
|
|
189
|
+
// Store never inspected → leave `skippedCandidates` absent rather than
|
|
190
|
+
// fabricating a clean 0.
|
|
174
191
|
}
|
|
175
192
|
|
|
176
193
|
const lastRunAt = _lastRunAt(candidates);
|
|
177
194
|
const lastRunCandidateCount = Array.isArray(candidates) ? candidates.length : 0;
|
|
178
195
|
const delta = entries.length - lastRunCandidateCount;
|
|
196
|
+
const quarantined = typeof skippedCandidates === 'number' ? skippedCandidates : 0;
|
|
179
197
|
|
|
180
198
|
const reasons = [];
|
|
181
|
-
// (a) — plenty of active learnings
|
|
199
|
+
// (a) — plenty of active learnings and no DATEABLE run on record. When the
|
|
200
|
+
// store holds quarantined lines the honest claim is "undeterminable", not
|
|
201
|
+
// "never": the evidence exists, it is merely unreadable. /reconcile is still
|
|
202
|
+
// the right action either way — mergeCandidates rewrites the store in full and
|
|
203
|
+
// purges the bad lines — so the nudge fires in both cases, only the wording
|
|
204
|
+
// differs.
|
|
182
205
|
if (active.length >= NUDGE_MIN_LEARNINGS && lastRunAt === null) {
|
|
183
|
-
reasons.push(
|
|
206
|
+
reasons.push(
|
|
207
|
+
quarantined > 0
|
|
208
|
+
? `${active.length} active learnings; last reconcile run undeterminable — ${quarantined} unreadable record(s) in the candidate store`
|
|
209
|
+
: `${active.length} active learnings with no reconcile run on record`,
|
|
210
|
+
);
|
|
184
211
|
}
|
|
185
212
|
// (b) — a determinable prior run exists, and the corpus has grown meaningfully since.
|
|
186
213
|
if (lastRunAt !== null && delta > NUDGE_MIN_DELTA) {
|
|
@@ -191,7 +218,7 @@ export async function computeReconcileNudge(opts = {}) {
|
|
|
191
218
|
reasons.push(`${eligibleCount} rule-eligible learnings`);
|
|
192
219
|
}
|
|
193
220
|
|
|
194
|
-
|
|
221
|
+
const computed = {
|
|
195
222
|
totalLearnings: entries.length,
|
|
196
223
|
activeLearnings: active.length,
|
|
197
224
|
eligibleCount,
|
|
@@ -201,6 +228,12 @@ export async function computeReconcileNudge(opts = {}) {
|
|
|
201
228
|
nudge: reasons.length > 0,
|
|
202
229
|
reasons,
|
|
203
230
|
};
|
|
231
|
+
// Additive + absence-preserving: the key exists ONLY when the candidate store
|
|
232
|
+
// was actually inspected, so no consumer can read a false `skippedCandidates: 0`.
|
|
233
|
+
if (typeof skippedCandidates === 'number') {
|
|
234
|
+
/** @type {any} */ (computed).skippedCandidates = skippedCandidates;
|
|
235
|
+
}
|
|
236
|
+
return computed;
|
|
204
237
|
}
|
|
205
238
|
|
|
206
239
|
/**
|
|
@@ -261,10 +294,33 @@ export async function checkReconcileNudge(opts = {}) {
|
|
|
261
294
|
}
|
|
262
295
|
}
|
|
263
296
|
|
|
264
|
-
|
|
297
|
+
// Three-state last-run label. `never` is a claim about history and must only
|
|
298
|
+
// be made when the store was inspected and held nothing: a store whose lines
|
|
299
|
+
// were quarantined by the shape guard is EVIDENCE OF A RUN that can no longer
|
|
300
|
+
// be dated, so it reads `undeterminable` — saying "never" there would assert
|
|
301
|
+
// an absence the file on disk contradicts (GitLab #955 finding 2).
|
|
302
|
+
const quarantined =
|
|
303
|
+
Number.isFinite(computed.skippedCandidates) && computed.skippedCandidates > 0
|
|
304
|
+
? Number(computed.skippedCandidates)
|
|
305
|
+
: 0;
|
|
306
|
+
const dated =
|
|
265
307
|
typeof computed.lastRunAt === 'string' && computed.lastRunAt.length >= 10
|
|
266
308
|
? computed.lastRunAt.slice(0, 10)
|
|
267
|
-
:
|
|
309
|
+
: null;
|
|
310
|
+
let lastRunLabel;
|
|
311
|
+
if (dated !== null) {
|
|
312
|
+
// Partially contaminated: the date is real but derived only from the
|
|
313
|
+
// surviving records, so flag that it may under-report.
|
|
314
|
+
lastRunLabel =
|
|
315
|
+
quarantined > 0
|
|
316
|
+
? `${dated} (+${quarantined} unreadable record(s) — date may be stale)`
|
|
317
|
+
: dated;
|
|
318
|
+
} else {
|
|
319
|
+
lastRunLabel =
|
|
320
|
+
quarantined > 0
|
|
321
|
+
? `undeterminable (${quarantined} unreadable record(s) in the candidate store)`
|
|
322
|
+
: 'never';
|
|
323
|
+
}
|
|
268
324
|
|
|
269
325
|
const lines = [
|
|
270
326
|
`⚠ reconcile-nudge: ${computed.activeLearnings} active learnings, ${computed.eligibleCount} rule-eligible, ` +
|
|
@@ -47,14 +47,54 @@ const MACOS_HEALTHY_PRESSURE_PCT = 30;
|
|
|
47
47
|
* and return a verdict used by session-start Phase 4.5.
|
|
48
48
|
* @param {object} snapshot — output of probe()
|
|
49
49
|
* @param {object} thresholds — resource-thresholds block from parseSessionConfig
|
|
50
|
+
* @param {{heavyRepo?: boolean, agentsPerWave?: number|{default: number, [mode: string]: number}}} [options] — HR-003/HR-004
|
|
51
|
+
* preflight ceiling (baseline #60). When `heavyRepo` is true and `agentsPerWave`
|
|
52
|
+
* resolves to a number (see {@link resolveAgentsPerWaveCap}), `recommended_agents_per_wave_cap`
|
|
53
|
+
* is forced to at most that number REGARDLESS of the live-probe verdict — a
|
|
54
|
+
* static preflight ceiling, not a runtime signal. More-restrictive-wins:
|
|
55
|
+
* effective = min(existing cap ?? Infinity, agentsPerWave). Omitted entirely
|
|
56
|
+
* = today's behaviour (back-compat).
|
|
57
|
+
*
|
|
58
|
+
* `agentsPerWave` accepts either a plain number OR the `{default, <mode>: N}`
|
|
59
|
+
* object `_coerceInteger()` (scripts/lib/config/coercers.mjs) produces for
|
|
60
|
+
* the parenthetical override syntax (e.g. `agents-per-wave: 4 (deep: 18)`) —
|
|
61
|
+
* `skills/session-start/phase-4-5-resource-health.md` documents wiring
|
|
62
|
+
* `config['agents-per-wave']` straight into this option, so the object shape
|
|
63
|
+
* is a real input here, not a hypothetical one.
|
|
50
64
|
* @returns {{verdict: 'green'|'warn'|'degraded'|'critical', reasons: string[], recommended_agents_per_wave_cap: number|null}}
|
|
51
65
|
*/
|
|
52
|
-
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolve an `agentsPerWave` option value into a plain numeric cap, or `null`
|
|
69
|
+
* when no cap should apply. Mirrors `resolveApwCap()` in
|
|
70
|
+
* `../wave-resource-gate.mjs` — kept as a local pure helper here rather than
|
|
71
|
+
* a cross-module import since both sites are ≤10 lines and evolve
|
|
72
|
+
* independently per their own gate's options shape.
|
|
73
|
+
*
|
|
74
|
+
* `evaluate()` has no session-mode input in scope, so the object shape
|
|
75
|
+
* resolves to `cap.default` — the documented HR-003 convention writes the
|
|
76
|
+
* override as `<default> (mode: <higher-ceiling>)`, i.e. `default` is the
|
|
77
|
+
* MORE restrictive of the pair, so this can only under-apply a looser
|
|
78
|
+
* mode-specific ceiling, never let a heavy repo exceed its base cap.
|
|
79
|
+
*
|
|
80
|
+
* @param {number|{default: number, [mode: string]: number}|*} cap
|
|
81
|
+
* @returns {number|null}
|
|
82
|
+
*/
|
|
83
|
+
function resolveAgentsPerWaveCap(cap) {
|
|
84
|
+
if (typeof cap === 'number') return Number.isFinite(cap) ? cap : null;
|
|
85
|
+
if (cap !== null && typeof cap === 'object' && !Array.isArray(cap)) {
|
|
86
|
+
const def = cap.default;
|
|
87
|
+
return typeof def === 'number' && Number.isFinite(def) ? def : null;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function evaluate(snapshot, thresholds, options = {}) {
|
|
53
93
|
const reasons = [];
|
|
54
94
|
let verdict = 'green';
|
|
55
95
|
let cap = null;
|
|
56
96
|
|
|
57
|
-
const { ram_free_gb, cpu_load_pct, claude_processes_count, memory_pressure_pct_free, ram_available_gb } = snapshot;
|
|
97
|
+
const { ram_free_gb, cpu_load_pct, cpu_load_5m_pct, claude_processes_count, memory_pressure_pct_free, ram_available_gb } = snapshot;
|
|
58
98
|
const {
|
|
59
99
|
'ram-free-min-gb': ramMin,
|
|
60
100
|
'ram-free-critical-gb': ramCrit,
|
|
@@ -93,10 +133,24 @@ export function evaluate(snapshot, thresholds) {
|
|
|
93
133
|
reasons.push(`macOS memory_pressure healthy (${memory_pressure_pct_free}% free ≥ ${MACOS_HEALTHY_PRESSURE_PCT}%) — free-RAM signal suppressed (Pages-free underreports on Darwin).`);
|
|
94
134
|
}
|
|
95
135
|
|
|
96
|
-
|
|
136
|
+
// CPU axis (#943): the 1m load average systematically carries the decaying
|
|
137
|
+
// tail of the coordinator's own just-finished gate run (the caller sits right
|
|
138
|
+
// after the inter-wave Quality Gate by construction). When the probe supplied
|
|
139
|
+
// a numeric `cpu_load_5m_pct`, judge CPU on min(1m, 5m) — only-1m-high is a
|
|
140
|
+
// decaying transient (informational, no cap), both-high is genuine sustained
|
|
141
|
+
// load. Mirrors the memory_pressure healthy-suppression pattern above.
|
|
142
|
+
// `cpu_load_5m_pct` absent/null (legacy snapshots, Windows) → legacy 1m-only.
|
|
143
|
+
const has5mCpu = typeof cpu_load_5m_pct === 'number' && Number.isFinite(cpu_load_5m_pct);
|
|
144
|
+
const effectiveCpuPct = has5mCpu ? Math.min(cpu_load_pct, cpu_load_5m_pct) : cpu_load_pct;
|
|
145
|
+
if (effectiveCpuPct > cpuMax) {
|
|
97
146
|
if (verdict === 'green') verdict = 'warn';
|
|
98
147
|
cap = cap === null ? 2 : Math.min(cap, 2);
|
|
99
|
-
|
|
148
|
+
const detail = has5mCpu ? ` (min of 1m ${cpu_load_pct}% / 5m ${cpu_load_5m_pct}%)` : '';
|
|
149
|
+
reasons.push(`CPU load ${effectiveCpuPct}%${detail} above threshold ${cpuMax}% — capping agents-per-wave at 2.`);
|
|
150
|
+
} else if (has5mCpu && cpu_load_pct > cpuMax) {
|
|
151
|
+
// Informational only: 1m spike with a calm 5m average = decaying transient
|
|
152
|
+
// (typically the coordinator's own just-finished gate run). No cap.
|
|
153
|
+
reasons.push(`CPU 1m load ${cpu_load_pct}% above threshold ${cpuMax}% but 5m load ${cpu_load_5m_pct}% is below — decaying transient (likely the coordinator's own gate run); no cap (#943).`);
|
|
100
154
|
}
|
|
101
155
|
|
|
102
156
|
if (claude_processes_count !== null && claude_processes_count !== undefined && claude_processes_count >= concWarn) {
|
|
@@ -186,5 +240,17 @@ export function evaluate(snapshot, thresholds) {
|
|
|
186
240
|
cap = 0;
|
|
187
241
|
}
|
|
188
242
|
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// HR-003/HR-004 heavy-repo preflight ceiling (#60): a STATIC cap independent
|
|
245
|
+
// of the live-probe verdict. Applies only when heavyRepo is true and
|
|
246
|
+
// agentsPerWave is a finite number — more-restrictive-wins against whatever
|
|
247
|
+
// the live-probe signals already computed.
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
const { heavyRepo, agentsPerWave } = options;
|
|
250
|
+
const resolvedApwCap = resolveAgentsPerWaveCap(agentsPerWave);
|
|
251
|
+
if (heavyRepo === true && resolvedApwCap !== null) {
|
|
252
|
+
cap = cap === null ? resolvedApwCap : Math.min(cap, resolvedApwCap);
|
|
253
|
+
}
|
|
254
|
+
|
|
189
255
|
return { verdict, reasons, recommended_agents_per_wave_cap: cap };
|
|
190
256
|
}
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* ram_used_pct: 78,
|
|
16
16
|
* cpu_load_1m: 2.4,
|
|
17
17
|
* cpu_load_pct: 65,
|
|
18
|
+
* cpu_load_5m: 1.8,
|
|
19
|
+
* cpu_load_5m_pct: 45 | null, // 5m load-average as pct-of-cores; null on Windows/zero-load (#943)
|
|
18
20
|
* claude_processes_count: 3 | null,
|
|
19
21
|
* codex_processes_count: 0 | null,
|
|
20
22
|
* other_node_processes: 12 | null,
|
|
@@ -37,6 +39,7 @@
|
|
|
37
39
|
* - scripts/lib/resource-probe/evaluate.mjs — verdict + threshold logic
|
|
38
40
|
*/
|
|
39
41
|
|
|
42
|
+
import os from 'node:os';
|
|
40
43
|
import { ramSnapshot, cpuSnapshot, processCounts, swapUsedMb, memoryPressurePctFree, ramAvailableGb } from './resource-probe/probe-platform.mjs';
|
|
41
44
|
|
|
42
45
|
// ---------------------------------------------------------------------------
|
|
@@ -63,6 +66,20 @@ export async function probe(opts = {}) {
|
|
|
63
66
|
const start = Date.now();
|
|
64
67
|
const ram = ramSnapshot();
|
|
65
68
|
const cpu = cpuSnapshot();
|
|
69
|
+
|
|
70
|
+
// #943: additionally sample the 5-minute load average. The wave-resource-gate
|
|
71
|
+
// runs, by construction, right after the coordinator's own CPU-saturating
|
|
72
|
+
// quality-gate run — the 1m average still carries that decaying tail
|
|
73
|
+
// (observed 2026-07-30: 96% → 91% → 78% → 75% within 36s after a Full Gate at
|
|
74
|
+
// 813% CPU), while the 5m average smooths it. Consumers (evaluate(),
|
|
75
|
+
// wave-resource-gate) judge CPU on min(1m, 5m) when the 5m signal exists.
|
|
76
|
+
// Windows reports loadavg [0,0,0] → cpu_load_5m_pct stays null there and
|
|
77
|
+
// consumers fall back to cpu_load_pct alone (which cpuSnapshot() derives from
|
|
78
|
+
// per-core times on Windows).
|
|
79
|
+
const load5m = os.loadavg()[1];
|
|
80
|
+
const cpuCores = (os.cpus() || []).length || 1;
|
|
81
|
+
const cpu_load_5m = Math.round(load5m * 10) / 10;
|
|
82
|
+
const cpu_load_5m_pct = load5m > 0 ? Math.min(100, Math.round((load5m / cpuCores) * 100)) : null;
|
|
66
83
|
const zombieThresholdMin = opts.zombieThresholdMin ?? null;
|
|
67
84
|
const procs = opts.skipProcessCounts
|
|
68
85
|
? { claude_processes_count: null, codex_processes_count: null, other_node_processes: null, zombie_processes_count: null }
|
|
@@ -84,6 +101,8 @@ export async function probe(opts = {}) {
|
|
|
84
101
|
timestamp: new Date().toISOString(),
|
|
85
102
|
...ram,
|
|
86
103
|
...cpu,
|
|
104
|
+
cpu_load_5m,
|
|
105
|
+
cpu_load_5m_pct,
|
|
87
106
|
...procs,
|
|
88
107
|
swap_used_mb,
|
|
89
108
|
memory_pressure_pct_free,
|
|
@@ -88,6 +88,12 @@ function getPicomatch() {
|
|
|
88
88
|
* Minimal glob-to-RegExp fallback used only when picomatch is absent.
|
|
89
89
|
* Handles `**`, `*`, and literal character matching.
|
|
90
90
|
*
|
|
91
|
+
* Drift contract (W4 B2): a semantically FROZEN twin of this function lives in
|
|
92
|
+
* scripts/lib/command-blocker.mjs (`redirectGlobToRegExp`) — duplicated there
|
|
93
|
+
* because that module is hook-hot-path pure and must not import this one.
|
|
94
|
+
* Syntax extensions made here do NOT automatically apply there; mirror them
|
|
95
|
+
* deliberately or document the divergence.
|
|
96
|
+
*
|
|
91
97
|
* @param {string} pattern
|
|
92
98
|
* @returns {RegExp}
|
|
93
99
|
*/
|