session-orchestrator 3.17.0 → 3.19.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 +105 -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/docs/USER-GUIDE.md +8 -3
- 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 +444 -0
- package/docs/rule-authoring.md +58 -9
- package/docs/session-config-reference.md +244 -9
- package/docs/session-config-template.md +39 -3
- package/hooks/_lib/guard-source-loader.mjs +467 -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 +10 -2
- 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 +23 -0
- 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 +525 -160
- package/hooks/pre-bash-issue-budget.mjs +167 -0
- package/hooks/pre-bash-sessions-ledger-guard.mjs +627 -0
- package/hooks/pre-bash-templates-first.mjs +96 -63
- package/hooks/subagent-telemetry.mjs +527 -37
- package/package.json +5 -2
- 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/check-doc-consistency.sh +17 -1
- 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 +1013 -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/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/schema.mjs +43 -3
- 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 +23 -4
- package/scripts/lib/reconcile/engine.mjs +147 -39
- package/scripts/lib/reconcile/idempotency.mjs +114 -14
- 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 +399 -98
- package/scripts/lib/session-close-backfill.mjs +61 -6
- 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-schema/constants.mjs +22 -3
- package/scripts/lib/session-schema/validator.mjs +16 -0
- 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/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/validate/check-agents.mjs +56 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +244 -10
- package/scripts/lib/validate/check-rules.mjs +217 -35
- 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-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 +51 -12
- package/scripts/release.mjs +534 -0
- package/scripts/run-quality-gate.mjs +123 -5
- 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/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 +1 -0
- package/skills/gitlab-ops/SKILL.md +20 -12
- package/skills/gitlab-portfolio/SKILL.md +2 -2
- 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/quality-gates/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +21 -4
- package/skills/session-end/SKILL.md +34 -13
- 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 +2 -1
- package/skills/session-end/plan-verification.md +5 -2
- 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 +73 -7
- package/skills/session-start/phase-4-5-resource-health.md +15 -2
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/vault-sync/validator.mjs +108 -7
- package/skills/wave-executor/SKILL.md +5 -2
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/wave-loop.md +163 -10
- package/templates/_shared/loop.md +4 -4
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vcs-repo-spec.mjs — resolve a glab/gh `-R`/`--repo` host-pinning spec from
|
|
3
|
+
* the local git remotes (#839).
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: a bare `glab`/`gh` spawn (no shell wrapper, no `-R`) falls
|
|
6
|
+
* back to the ambient `GITLAB_HOST`/`GH_HOST` env var to pick a host. On a
|
|
7
|
+
* multi-GitLab-instance host that can resolve to the WRONG instance and fail
|
|
8
|
+
* silently — the root cause of #839.
|
|
9
|
+
*
|
|
10
|
+
* Injecting `GITLAB_HOST` into the spawn env is NOT the fix — it was tested
|
|
11
|
+
* live and FAILS whenever `~/.ssh/config` maps the GitLab hostname to a
|
|
12
|
+
* `HostName` IP alias: glab then reports "none of the git remotes configured
|
|
13
|
+
* for this repository correspond to the GITLAB_HOST environment variable"
|
|
14
|
+
* because the remote it sees is the IP, not the hostname. The only mechanism
|
|
15
|
+
* verified to survive that mismatch is passing the raw remote URL (or a
|
|
16
|
+
* HOST/OWNER/REPO spec) via `-R`/`--repo`.
|
|
17
|
+
*
|
|
18
|
+
* Preference order:
|
|
19
|
+
* - vcs === 'github': remote `github` → `origin`
|
|
20
|
+
* - vcs === 'gitlab' (default): remote `gitlab` → `origin`
|
|
21
|
+
*
|
|
22
|
+
* Returns `undefined` when no matching remote resolves — callers MUST treat
|
|
23
|
+
* this as "could not auto-detect" and omit the `-R`/`--repo` flag entirely
|
|
24
|
+
* (never emit `-R undefined`).
|
|
25
|
+
*
|
|
26
|
+
* Spec format (#872 follow-up): `resolveRepoSpec` returns the RAW remote URL
|
|
27
|
+
* for `vcs: 'gitlab'`, but a NORMALIZED `HOST/OWNER/REPO` string for
|
|
28
|
+
* `vcs: 'github'` — `gh -R`/`--repo` documents only `[HOST/]OWNER/REPO` as
|
|
29
|
+
* its accepted spec shape, unlike `glab -R` which explicitly accepts a full
|
|
30
|
+
* URL. See `resolveRepoSpec`'s own docblock for the full rationale. This
|
|
31
|
+
* module also exports `resolveRepoHost` for the `glab api`/`gh api`
|
|
32
|
+
* call sites, which accept neither `-R` nor a URL — only `--hostname`.
|
|
33
|
+
*
|
|
34
|
+
* Credential safety (#907, CWE-214): every value returned by `resolveRepoSpec`
|
|
35
|
+
* / `resolveRepoHost` has any embedded userinfo credential
|
|
36
|
+
* (`https://user:token@host/...`, the GitLab-CI checkout pattern) stripped at
|
|
37
|
+
* the source, so a credential can never reach a `-R`/`--repo`/`--hostname`
|
|
38
|
+
* argv position (visible via `ps` / `/proc/<pid>/cmdline`). A scp-like SSH
|
|
39
|
+
* login (`git@host:path`) is NOT a credential and is preserved verbatim. The
|
|
40
|
+
* exported `redactUrlCredentials` is the log-line defense-in-depth counterpart
|
|
41
|
+
* for values that bypass the source strip (e.g. a `--repo` override). See
|
|
42
|
+
* `stripUrlCredentials` / `userinfoIsCredential`.
|
|
43
|
+
*
|
|
44
|
+
* Lifted out of `scripts/archive-closed-prds.mjs::defaultGlabRepo` (that
|
|
45
|
+
* script's docblock described this exact problem months before #839 was
|
|
46
|
+
* filed) into a shared `scripts/lib/` module so
|
|
47
|
+
* `scripts/lib/issue-close-strip-labels.mjs` and
|
|
48
|
+
* `scripts/lib/spiral-carryover.mjs` can reuse the same, single
|
|
49
|
+
* implementation instead of each re-deriving it.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import { execFileSync } from 'node:child_process';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* argv-boundary guard (#872 follow-up, Q3-LOW centralization). A well-formed
|
|
56
|
+
* git remote URL or bare hostname never legitimately contains whitespace or a
|
|
57
|
+
* C0 control character — if a resolved value does, treat it as `undefined`
|
|
58
|
+
* ("could not auto-detect") rather than let it flow into a `-R`/`--repo`/
|
|
59
|
+
* `--hostname` argv position. This is the CENTRAL defense for both
|
|
60
|
+
* `resolveRepoSpec` and `resolveRepoHost`: the ~9 call sites across the repo
|
|
61
|
+
* that do a bare `if (spec) args.push('-R', spec)` do not re-check the value
|
|
62
|
+
* themselves, so they inherit this guard for free by going through either
|
|
63
|
+
* exported function here rather than reading a git remote URL directly.
|
|
64
|
+
*/
|
|
65
|
+
// eslint-disable-next-line no-control-regex -- deliberate: the argv-boundary guard must catch ALL C0 control characters, not only the \s subset
|
|
66
|
+
const UNSAFE_ARGV_CHARS_RE = /[\s\x00-\x1f]/;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {unknown} value
|
|
70
|
+
* @returns {boolean} true when `value` is a string containing whitespace or a
|
|
71
|
+
* C0 control character — unsafe to forward as a single argv token.
|
|
72
|
+
*/
|
|
73
|
+
function isUnsafeForArgv(value) {
|
|
74
|
+
return typeof value === 'string' && UNSAFE_ARGV_CHARS_RE.test(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Embedded-credential guard (#907, CWE-214). A `scheme://userinfo@host/...`
|
|
79
|
+
* remote URL carries its userinfo (`user:password`, or a bare `token`) in the
|
|
80
|
+
* clear. The GitLab-CI checkout pattern
|
|
81
|
+
* `https://gitlab-ci-token:<MASKED>@host/group/project.git` is the canonical
|
|
82
|
+
* source. If that raw URL reaches a `-R`/`--repo` argv position it is visible
|
|
83
|
+
* via `ps` / `/proc/<pid>/cmdline`, and if it reaches a `--verbose` log line
|
|
84
|
+
* it is written to CI job output — either way the credential leaks. The
|
|
85
|
+
* 2026-07-26 argv-boundary guard ({@link UNSAFE_ARGV_CHARS_RE}) does NOT catch
|
|
86
|
+
* this: an embedded credential contains no whitespace or control character.
|
|
87
|
+
*
|
|
88
|
+
* Matches ONLY the `scheme://` URL forms (`https://`, `http://`, `ssh://`,
|
|
89
|
+
* `git+ssh://`, …). The scp-like SSH form `git@host:path` has no `://` and is
|
|
90
|
+
* therefore never matched — a bare SSH login user is not a credential. The
|
|
91
|
+
* userinfo class `[^/\s]+` stops at the first `/`, so an `@` that appears in a
|
|
92
|
+
* PATH (e.g. `.../path@ref`) is never mistaken for userinfo. Because `@` is NOT
|
|
93
|
+
* excluded from the class, a greedy match binds `@` to the LAST `@` before the
|
|
94
|
+
* authority ends — the real userinfo/host separator per RFC 3986 (and how
|
|
95
|
+
* glab/gh parse it). This closes the residual-credential leak where a raw `@`
|
|
96
|
+
* inside the token/password (`user:gl@token@host`, or nested `a:b@c:d@host`)
|
|
97
|
+
* previously left a partial secret after a first-`@`-only match (#907 MED-1).
|
|
98
|
+
*/
|
|
99
|
+
const URL_WITH_USERINFO_RE = /([a-z][a-z0-9+.-]*:\/\/)([^/\s]+)@/gi;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Decide whether a matched `scheme://userinfo@` is a CREDENTIAL (strip/redact)
|
|
103
|
+
* or a legitimate login username (leave untouched).
|
|
104
|
+
*
|
|
105
|
+
* A credential is either:
|
|
106
|
+
* - any userinfo carrying a password component (`user:pass`, a `:` present) —
|
|
107
|
+
* for ANY scheme, including `ssh://user:pass@host`; or
|
|
108
|
+
* - a bare userinfo on an `http`/`https` scheme (`https://token@host`) — HTTPS
|
|
109
|
+
* git auth passes tokens/PATs through the userinfo slot, so a bare userinfo
|
|
110
|
+
* there is a token, never a plain username.
|
|
111
|
+
*
|
|
112
|
+
* A bare userinfo on a non-HTTP scheme (`ssh://git@host`) is a login username,
|
|
113
|
+
* NOT a credential — SSH authenticates with keys, never a URL-embedded secret —
|
|
114
|
+
* so it is left untouched, consistent with the scp-like `git@host:path` case.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} scheme e.g. `https://` (includes the trailing `://`)
|
|
117
|
+
* @param {string} userinfo the substring between `scheme` and `@`
|
|
118
|
+
* @returns {boolean}
|
|
119
|
+
*/
|
|
120
|
+
function userinfoIsCredential(scheme, userinfo) {
|
|
121
|
+
if (userinfo.includes(':')) return true;
|
|
122
|
+
return /^https?:\/\/$/i.test(scheme);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Strip credential userinfo from a single remote URL, returning the URL with
|
|
127
|
+
* host/path/project-spec EXACTLY preserved. A credential-free URL (and the
|
|
128
|
+
* scp-like `git@host:path` SSH form) is returned BYTE-IDENTICAL — the strip is
|
|
129
|
+
* a no-op unless {@link userinfoIsCredential} classifies the userinfo as a
|
|
130
|
+
* secret. Never throws; a non-string returns unchanged.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} url
|
|
133
|
+
* @returns {string}
|
|
134
|
+
*/
|
|
135
|
+
export function stripUrlCredentials(url) {
|
|
136
|
+
if (typeof url !== 'string') return url;
|
|
137
|
+
return url.replace(URL_WITH_USERINFO_RE, (match, scheme, userinfo) =>
|
|
138
|
+
userinfoIsCredential(scheme, userinfo) ? scheme : match,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Defense-in-depth redactor for LOG output: replace any credential userinfo in
|
|
144
|
+
* an arbitrary text string (e.g. a verbose `glab … -R <spec>` line) with a
|
|
145
|
+
* `***` marker, leaving the surrounding text and the URL host/path intact. Used
|
|
146
|
+
* by `scripts/vault-integration-watcher.mjs`'s `verbose()` to cover the
|
|
147
|
+
* `--repo <spec>` override path, which bypasses the source-level strip in
|
|
148
|
+
* {@link resolveRawRemoteUrl}. Never throws; a non-string returns unchanged.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} text
|
|
151
|
+
* @returns {string}
|
|
152
|
+
*/
|
|
153
|
+
export function redactUrlCredentials(text) {
|
|
154
|
+
if (typeof text !== 'string') return text;
|
|
155
|
+
return text.replace(URL_WITH_USERINFO_RE, (match, scheme, userinfo) =>
|
|
156
|
+
userinfoIsCredential(scheme, userinfo) ? `${scheme}***@` : match,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Default git-remote runner: `git -C <repoRoot> remote get-url <name>`.
|
|
162
|
+
* Never throws — returns `{ ok:false, stdout:'', stderr }` on any failure
|
|
163
|
+
* (missing remote, not a git repo, git not on PATH, ...).
|
|
164
|
+
*
|
|
165
|
+
* @param {string[]} gitArgs
|
|
166
|
+
* @returns {{ ok: boolean, stdout: string, stderr: string }}
|
|
167
|
+
*/
|
|
168
|
+
function defaultGitRun(gitArgs) {
|
|
169
|
+
try {
|
|
170
|
+
const stdout = execFileSync('git', gitArgs, {
|
|
171
|
+
encoding: 'utf8',
|
|
172
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
173
|
+
});
|
|
174
|
+
return { ok: true, stdout: String(stdout ?? ''), stderr: '' };
|
|
175
|
+
} catch (err) {
|
|
176
|
+
const stderr =
|
|
177
|
+
err && err.stderr ? String(err.stderr) : err && err.message ? String(err.message) : 'unknown error';
|
|
178
|
+
return { ok: false, stdout: '', stderr };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Remote-name preference order, per VCS. */
|
|
183
|
+
const REMOTE_PREFERENCE = {
|
|
184
|
+
gitlab: ['gitlab', 'origin'],
|
|
185
|
+
github: ['github', 'origin'],
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The OTHER platform's well-known public host, per requested vcs — the one
|
|
190
|
+
* case a resolved remote URL can be PROVEN to belong to the wrong VCS family
|
|
191
|
+
* without any repo-specific host configuration (self-hosted GitLab/GitHub
|
|
192
|
+
* Enterprise instances can live at ANY domain, so this deliberately checks
|
|
193
|
+
* only the unambiguous public-host case, not a general host allow-list).
|
|
194
|
+
*/
|
|
195
|
+
const WRONG_FAMILY_HOST = {
|
|
196
|
+
gitlab: 'github.com',
|
|
197
|
+
github: 'gitlab.com',
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Extract the bare hostname from a git remote URL, handling both the HTTPS
|
|
202
|
+
* (`https://host/owner/repo.git`) and SSH (`git@host:owner/repo.git`) forms.
|
|
203
|
+
* Returns `null` for an unrecognized shape (never throws).
|
|
204
|
+
*
|
|
205
|
+
* @param {string} url
|
|
206
|
+
* @returns {string|null}
|
|
207
|
+
*/
|
|
208
|
+
function extractHost(url) {
|
|
209
|
+
const httpsMatch = /^https?:\/\/([^/]+)/i.exec(url);
|
|
210
|
+
if (httpsMatch) return httpsMatch[1].toLowerCase();
|
|
211
|
+
const sshMatch = /^[^@\s]+@([^:\s]+):/i.exec(url);
|
|
212
|
+
if (sshMatch) return sshMatch[1].toLowerCase();
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Normalize a github remote URL (HTTPS or SSH) into the `HOST/OWNER/REPO`
|
|
218
|
+
* shape `gh -R`/`--repo` documents as its accepted spec format. gh does NOT
|
|
219
|
+
* accept a raw remote URL the way `glab -R` does — only `[HOST/]OWNER/REPO`.
|
|
220
|
+
* Strips a trailing `.git` suffix and any trailing slash.
|
|
221
|
+
*
|
|
222
|
+
* Falls back to returning `url` unchanged when it does not match the
|
|
223
|
+
* expected `host/owner/repo` shape (never throws) — a raw URL is still
|
|
224
|
+
* strictly better than omitting `-R` entirely.
|
|
225
|
+
*
|
|
226
|
+
* @param {string} url
|
|
227
|
+
* @returns {string}
|
|
228
|
+
*/
|
|
229
|
+
function normalizeGithubSpec(url) {
|
|
230
|
+
const httpsMatch = /^https?:\/\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i.exec(url);
|
|
231
|
+
if (httpsMatch) {
|
|
232
|
+
const [, host, owner, repo] = httpsMatch;
|
|
233
|
+
return `${host.toLowerCase()}/${owner}/${repo}`;
|
|
234
|
+
}
|
|
235
|
+
const sshMatch = /^[^@\s]+@([^:\s]+):([^/]+)\/([^/]+?)(?:\.git)?\/?$/i.exec(url);
|
|
236
|
+
if (sshMatch) {
|
|
237
|
+
const [, host, owner, repo] = sshMatch;
|
|
238
|
+
return `${host.toLowerCase()}/${owner}/${repo}`;
|
|
239
|
+
}
|
|
240
|
+
return url;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Resolve the raw remote URL for the requested vcs — shared by
|
|
245
|
+
* `resolveRepoSpec` and `resolveRepoHost` so both apply the identical
|
|
246
|
+
* remote-preference-order + cross-family-guard resolution.
|
|
247
|
+
*
|
|
248
|
+
* Cross-family guard (#839 follow-up): a candidate URL whose host is the
|
|
249
|
+
* OTHER platform's well-known public host (`github.com` under vcs:'gitlab',
|
|
250
|
+
* `gitlab.com` under vcs:'github') is skipped rather than returned — passing
|
|
251
|
+
* it to `glab`/`gh -R` would be a guaranteed hard failure, strictly worse
|
|
252
|
+
* than the ambient-resolution fallback #839 replaced. This is a narrow,
|
|
253
|
+
* unambiguous check only; it does not attempt to validate self-hosted
|
|
254
|
+
* domains, which cannot be distinguished from a URL string alone.
|
|
255
|
+
*
|
|
256
|
+
* Credential guard (#907, CWE-214): the raw `git remote get-url` output can
|
|
257
|
+
* be `https://user:token@host/...` (GitLab-CI checkout pattern). The userinfo
|
|
258
|
+
* is stripped HERE, at the single source both `resolveRepoSpec` and
|
|
259
|
+
* `resolveRepoHost` flow through, BEFORE the cross-family host check and
|
|
260
|
+
* before the value can reach any `-R`/`--repo`/`--hostname` argv position —
|
|
261
|
+
* see {@link stripUrlCredentials}. A credential-free URL is unchanged
|
|
262
|
+
* (byte-identical), so #839/#872 behaviour is preserved.
|
|
263
|
+
*
|
|
264
|
+
* @param {{
|
|
265
|
+
* repoRoot?: string,
|
|
266
|
+
* vcs?: 'gitlab' | 'github',
|
|
267
|
+
* gitRun?: (args: string[]) => { ok: boolean, stdout: string, stderr: string }
|
|
268
|
+
* }} [opts]
|
|
269
|
+
* @returns {string|undefined}
|
|
270
|
+
*/
|
|
271
|
+
function resolveRawRemoteUrl({ repoRoot, vcs = 'gitlab', gitRun = defaultGitRun } = {}) {
|
|
272
|
+
const vcsResolved = vcs === 'github' ? 'github' : 'gitlab';
|
|
273
|
+
const root = repoRoot ?? process.cwd();
|
|
274
|
+
const wrongFamilyHost = WRONG_FAMILY_HOST[vcsResolved];
|
|
275
|
+
|
|
276
|
+
for (const remote of REMOTE_PREFERENCE[vcsResolved]) {
|
|
277
|
+
const { ok, stdout } = gitRun(['-C', root, 'remote', 'get-url', remote]);
|
|
278
|
+
const url = ok ? stripUrlCredentials(stdout.trim()) : '';
|
|
279
|
+
if (!url) continue;
|
|
280
|
+
if (extractHost(url) === wrongFamilyHost) continue;
|
|
281
|
+
return url;
|
|
282
|
+
}
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Resolve the glab/gh `-R`/`--repo` host-pinning spec.
|
|
288
|
+
*
|
|
289
|
+
* Format contract differs by vcs, because `glab -R` and `gh -R` accept
|
|
290
|
+
* different spec shapes:
|
|
291
|
+
* - `vcs: 'gitlab'` (default): the RAW remote URL, verbatim. `glab -R`
|
|
292
|
+
* explicitly accepts a full URL, and GitLab group namespaces can nest
|
|
293
|
+
* arbitrarily deep (`group/subgroup/project`), which makes a reliable
|
|
294
|
+
* `OWNER/REPO` derivation impossible from the URL alone — so the raw URL
|
|
295
|
+
* is the only unambiguous spec here.
|
|
296
|
+
* - `vcs: 'github'`: the NORMALIZED `HOST/OWNER/REPO` form (see
|
|
297
|
+
* `normalizeGithubSpec`). `gh -R`/`--repo` documents ONLY
|
|
298
|
+
* `[HOST/]OWNER/REPO` as accepted input — a raw URL is not guaranteed to
|
|
299
|
+
* parse the same way, and GitHub repos are always exactly two path
|
|
300
|
+
* segments (owner/repo), so the derivation is unambiguous.
|
|
301
|
+
*
|
|
302
|
+
* Returns `undefined` when no matching remote resolves — callers MUST treat
|
|
303
|
+
* this as "could not auto-detect" and omit the `-R`/`--repo` flag entirely
|
|
304
|
+
* (never emit `-R undefined`). Also returns `undefined` when the resolved
|
|
305
|
+
* spec contains whitespace or a control character — see the module-level
|
|
306
|
+
* argv-boundary guard ({@link isUnsafeForArgv}), which this function applies
|
|
307
|
+
* to the FINAL spec value (post `normalizeGithubSpec`, when applicable).
|
|
308
|
+
*
|
|
309
|
+
* @param {{
|
|
310
|
+
* repoRoot?: string,
|
|
311
|
+
* vcs?: 'gitlab' | 'github',
|
|
312
|
+
* gitRun?: (args: string[]) => { ok: boolean, stdout: string, stderr: string }
|
|
313
|
+
* }} [opts]
|
|
314
|
+
* @returns {string|undefined}
|
|
315
|
+
*/
|
|
316
|
+
export function resolveRepoSpec({ repoRoot, vcs = 'gitlab', gitRun = defaultGitRun } = {}) {
|
|
317
|
+
const vcsResolved = vcs === 'github' ? 'github' : 'gitlab';
|
|
318
|
+
const url = resolveRawRemoteUrl({ repoRoot, vcs: vcsResolved, gitRun });
|
|
319
|
+
if (!url) return undefined;
|
|
320
|
+
const spec = vcsResolved === 'github' ? normalizeGithubSpec(url) : url;
|
|
321
|
+
return isUnsafeForArgv(spec) ? undefined : spec;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Resolve the bare hostname of the matching remote, for use with
|
|
326
|
+
* `glab api --hostname`/`gh api --hostname` — the `api` subcommand of both
|
|
327
|
+
* CLIs does NOT accept `-R`/`--repo` (it has no repo concept), only a
|
|
328
|
+
* `--hostname` flag to pin which instance the request targets. This is the
|
|
329
|
+
* host-pinning counterpart to `resolveRepoSpec` for those api-only call
|
|
330
|
+
* sites.
|
|
331
|
+
*
|
|
332
|
+
* Applies the identical remote-preference-order + cross-family-guard
|
|
333
|
+
* resolution as `resolveRepoSpec`, just returning the host instead of the
|
|
334
|
+
* full spec — same contract: `undefined` ⇒ caller omits the flag entirely.
|
|
335
|
+
* Also applies the same argv-boundary guard ({@link isUnsafeForArgv}) to the
|
|
336
|
+
* resolved host before returning it.
|
|
337
|
+
*
|
|
338
|
+
* @param {{
|
|
339
|
+
* repoRoot?: string,
|
|
340
|
+
* vcs?: 'gitlab' | 'github',
|
|
341
|
+
* gitRun?: (args: string[]) => { ok: boolean, stdout: string, stderr: string }
|
|
342
|
+
* }} [opts]
|
|
343
|
+
* @returns {string|undefined}
|
|
344
|
+
*/
|
|
345
|
+
export function resolveRepoHost({ repoRoot, vcs, gitRun } = {}) {
|
|
346
|
+
const url = resolveRawRemoteUrl({ repoRoot, vcs, gitRun });
|
|
347
|
+
const host = url ? (extractHost(url) ?? undefined) : undefined;
|
|
348
|
+
return isUnsafeForArgv(host) ? undefined : host;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* @deprecated Back-compat alias for `archive-closed-prds.mjs`'s original
|
|
353
|
+
* `defaultGlabRepo(repoRoot, gitRunFn)` positional signature (gitlab-only).
|
|
354
|
+
* New callers should use `resolveRepoSpec`.
|
|
355
|
+
*
|
|
356
|
+
* @param {string} repoRoot
|
|
357
|
+
* @param {(args: string[]) => { ok: boolean, stdout: string, stderr: string }} gitRunFn
|
|
358
|
+
* @returns {string|undefined}
|
|
359
|
+
*/
|
|
360
|
+
export function defaultGlabRepo(repoRoot, gitRunFn) {
|
|
361
|
+
return resolveRepoSpec({ repoRoot, vcs: 'gitlab', gitRun: gitRunFn });
|
|
362
|
+
}
|
|
@@ -20,7 +20,7 @@ import { probe } from './resource-probe.mjs';
|
|
|
20
20
|
* "proceed" decision.
|
|
21
21
|
*
|
|
22
22
|
* @param {object} opts - Same opts shape as evaluateWaveResourceGate
|
|
23
|
-
* @returns {Promise<{ramFreeGb: number, ramAvailableGb: number|null, cpuLoadPct: number, concurrentSessions: number} | {probeFailed: true}>}
|
|
23
|
+
* @returns {Promise<{ramFreeGb: number, ramAvailableGb: number|null, cpuLoadPct: number, cpuLoad5mPct: number|null, concurrentSessions: number} | {probeFailed: true}>}
|
|
24
24
|
*/
|
|
25
25
|
async function extractMeasurements(opts) {
|
|
26
26
|
const { probeOverride } = opts;
|
|
@@ -31,6 +31,8 @@ async function extractMeasurements(opts) {
|
|
|
31
31
|
// Tests may supply ramAvailableGb to exercise the macOS path; absent → null.
|
|
32
32
|
ramAvailableGb: probeOverride.ramAvailableGb ?? null,
|
|
33
33
|
cpuLoadPct: probeOverride.cpuLoadPct,
|
|
34
|
+
// 5m-average CPU pct (#943); absent → null (legacy overrides → 1m-only judging).
|
|
35
|
+
cpuLoad5mPct: probeOverride.cpuLoad5mPct ?? null,
|
|
34
36
|
concurrentSessions: probeOverride.concurrentSessions,
|
|
35
37
|
};
|
|
36
38
|
}
|
|
@@ -47,6 +49,9 @@ async function extractMeasurements(opts) {
|
|
|
47
49
|
// os.freemem() is already accurate. (#667)
|
|
48
50
|
ramAvailableGb: snapshot.ram_available_gb ?? null,
|
|
49
51
|
cpuLoadPct: snapshot.cpu_load_pct,
|
|
52
|
+
// 5m load-average as pct-of-cores (#943). null on Windows/zero-load, where
|
|
53
|
+
// the gate falls back to judging the 1m-derived cpu_load_pct alone.
|
|
54
|
+
cpuLoad5mPct: snapshot.cpu_load_5m_pct ?? null,
|
|
50
55
|
// concurrent sessions: number of claude processes found by the probe.
|
|
51
56
|
concurrentSessions: snapshot.claude_processes_count ?? 0,
|
|
52
57
|
};
|
|
@@ -54,15 +59,89 @@ async function extractMeasurements(opts) {
|
|
|
54
59
|
|
|
55
60
|
/**
|
|
56
61
|
* Apply the gate decision rule sequence (rules 3-8) given measurements and
|
|
57
|
-
* config
|
|
62
|
+
* config, then apply the HR-004 heavy-repo preflight ceiling on top. Returns
|
|
63
|
+
* the full gate result.
|
|
58
64
|
*
|
|
59
|
-
* @param {{ramFreeGb: number, cpuLoadPct: number, concurrentSessions: number}} measurements
|
|
65
|
+
* @param {{ramFreeGb: number, ramAvailableGb?: number|null, cpuLoadPct: number, cpuLoad5mPct?: number|null, concurrentSessions: number}} measurements
|
|
60
66
|
* @param {object} opts - Same opts shape as evaluateWaveResourceGate
|
|
61
67
|
* @returns {{decision: string, agents: number, reasons: string[], measurements: object}}
|
|
62
68
|
*/
|
|
63
69
|
function applyDecisionRules(measurements, opts) {
|
|
70
|
+
const result = computeResourceDecision(measurements, opts);
|
|
71
|
+
return applyHeavyRepoCap(result, opts);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve an `agents-per-wave` config value into a plain numeric cap, or
|
|
76
|
+
* `null` when no cap should apply.
|
|
77
|
+
*
|
|
78
|
+
* `_coerceInteger()` (scripts/lib/config/coercers.mjs) parses the documented
|
|
79
|
+
* HR-003 parenthetical-override syntax — `agents-per-wave: 4 (deep: 18)` —
|
|
80
|
+
* into an OBJECT `{ default: 4, deep: 18 }`, not a plain number. Feeding that
|
|
81
|
+
* object straight into a `typeof cap !== 'number'` guard makes the heavy-repo
|
|
82
|
+
* cap silently no-op for every repo using the override syntax, which defeats
|
|
83
|
+
* HR-004 exactly where it matters most (a heavy repo that also runs deep
|
|
84
|
+
* sessions).
|
|
85
|
+
*
|
|
86
|
+
* `evaluateWaveResourceGate()` has no session-mode input in scope — `waveRole`
|
|
87
|
+
* is a wave role (e.g. "Impl-Core"), not a session mode (e.g. "deep") — so the
|
|
88
|
+
* object shape resolves to `cap.default` here. That is the conservative
|
|
89
|
+
* choice: the documented HR-003 convention writes the override as
|
|
90
|
+
* `<default> (mode: <higher-ceiling>)`, i.e. `default` is the MORE
|
|
91
|
+
* restrictive of the pair. Falling back to it can only under-apply a looser
|
|
92
|
+
* mode-specific ceiling; it never lets a heavy repo exceed its base cap.
|
|
93
|
+
*
|
|
94
|
+
* @param {number|{default: number, [mode: string]: number}|*} cap
|
|
95
|
+
* @returns {number|null}
|
|
96
|
+
*/
|
|
97
|
+
function resolveApwCap(cap) {
|
|
98
|
+
if (typeof cap === 'number') return Number.isFinite(cap) ? cap : null;
|
|
99
|
+
if (cap !== null && typeof cap === 'object' && !Array.isArray(cap)) {
|
|
100
|
+
const def = cap.default;
|
|
101
|
+
return typeof def === 'number' && Number.isFinite(def) ? def : null;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* HR-003/HR-004 heavy-repo preflight ceiling (baseline #60). A STATIC cap
|
|
108
|
+
* independent of the live resource-probe verdict: when `config['heavy-repo']`
|
|
109
|
+
* is `true`, `agents` is clamped to at most `config['agents-per-wave']`
|
|
110
|
+
* (resolved via {@link resolveApwCap} to handle the object-override shape).
|
|
111
|
+
* More-restrictive-wins — this only ever LOWERS `agents`, never raises it
|
|
112
|
+
* above what the resource-driven rules already decided (e.g. a
|
|
113
|
+
* coordinator-direct 0 stays 0).
|
|
114
|
+
*
|
|
115
|
+
* @param {{decision: string, agents: number, reasons: string[], measurements: object}} result
|
|
116
|
+
* @param {object} opts - Same opts shape as evaluateWaveResourceGate
|
|
117
|
+
* @returns {{decision: string, agents: number, reasons: string[], measurements: object}}
|
|
118
|
+
*/
|
|
119
|
+
function applyHeavyRepoCap(result, opts) {
|
|
120
|
+
const { config } = opts;
|
|
121
|
+
if (!config || config['heavy-repo'] !== true) return result;
|
|
122
|
+
const cap = resolveApwCap(config['agents-per-wave']);
|
|
123
|
+
if (cap === null) return result;
|
|
124
|
+
if (result.agents <= cap) return result; // already within the ceiling — never raise
|
|
125
|
+
return {
|
|
126
|
+
...result,
|
|
127
|
+
decision: result.decision === 'coordinator-direct' ? 'coordinator-direct' : 'reduce',
|
|
128
|
+
agents: cap,
|
|
129
|
+
reasons: [...result.reasons, `heavy-repo: true caps agents-per-wave to ${cap} (HR-004)`],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Rules 3-8: resource-driven decision sequence (RAM/CPU/concurrent-sessions).
|
|
135
|
+
* Extracted so `applyDecisionRules` can layer the HR-004 heavy-repo cap on
|
|
136
|
+
* top without duplicating this sequence.
|
|
137
|
+
*
|
|
138
|
+
* @param {{ramFreeGb: number, ramAvailableGb?: number|null, cpuLoadPct: number, cpuLoad5mPct?: number|null, concurrentSessions: number}} measurements
|
|
139
|
+
* @param {object} opts - Same opts shape as evaluateWaveResourceGate
|
|
140
|
+
* @returns {{decision: string, agents: number, reasons: string[], measurements: object}}
|
|
141
|
+
*/
|
|
142
|
+
function computeResourceDecision(measurements, opts) {
|
|
64
143
|
const { config, plannedAgents } = opts;
|
|
65
|
-
const { ramFreeGb, ramAvailableGb, cpuLoadPct, concurrentSessions } = measurements;
|
|
144
|
+
const { ramFreeGb, ramAvailableGb, cpuLoadPct, cpuLoad5mPct, concurrentSessions } = measurements;
|
|
66
145
|
const T = config['resource-thresholds'];
|
|
67
146
|
|
|
68
147
|
// macOS fix (#667): os.freemem() reports only `Pages free`, which reads
|
|
@@ -111,24 +190,41 @@ function applyDecisionRules(measurements, opts) {
|
|
|
111
190
|
};
|
|
112
191
|
}
|
|
113
192
|
|
|
114
|
-
// Rule 6: CPU overloaded → reduce.
|
|
115
|
-
|
|
193
|
+
// Rule 6: CPU overloaded → reduce. #943: this gate runs, by construction,
|
|
194
|
+
// right after the coordinator's own CPU-saturating quality-gate run — the 1m
|
|
195
|
+
// load average still carries that decaying tail (observed 2026-07-30:
|
|
196
|
+
// 96% → 91% → 78% → 75% within 36s), so a 1m-only reading systematically
|
|
197
|
+
// over-reports and halves waves without a real bottleneck. When the probe
|
|
198
|
+
// supplied a numeric 5m percentage, judge on min(1m, 5m): only-1m-high is a
|
|
199
|
+
// decaying transient (informational, no reduce), both-high is genuine
|
|
200
|
+
// sustained load. `cpuLoad5mPct` null (legacy overrides, Windows) → 1m-only.
|
|
201
|
+
const has5mCpu = typeof cpuLoad5mPct === 'number' && Number.isFinite(cpuLoad5mPct);
|
|
202
|
+
const effectiveCpuLoadPct = has5mCpu ? Math.min(cpuLoadPct, cpuLoad5mPct) : cpuLoadPct;
|
|
203
|
+
if (effectiveCpuLoadPct > T['cpu-load-max-pct']) {
|
|
204
|
+
const detail = has5mCpu ? ` (min of 1m ${cpuLoadPct}% / 5m ${cpuLoad5mPct}%)` : '';
|
|
116
205
|
return {
|
|
117
206
|
decision: 'reduce',
|
|
118
207
|
agents: Math.max(1, Math.floor(plannedAgents / 2)),
|
|
119
208
|
reasons: [
|
|
120
|
-
`CPU load ${
|
|
209
|
+
`CPU load ${effectiveCpuLoadPct}%${detail} > max ${T['cpu-load-max-pct']}% — reducing agent count`,
|
|
121
210
|
],
|
|
122
211
|
measurements,
|
|
123
212
|
};
|
|
124
213
|
}
|
|
214
|
+
const cpuTransientNote =
|
|
215
|
+
has5mCpu && cpuLoadPct > T['cpu-load-max-pct']
|
|
216
|
+
? `info: CPU 1m load ${cpuLoadPct}% > max ${T['cpu-load-max-pct']}% but 5m load ${cpuLoad5mPct}% is below — decaying transient (typically the coordinator's own just-finished gate run), not reducing (#943)`
|
|
217
|
+
: null;
|
|
125
218
|
|
|
126
219
|
// Rule 7: concurrent sessions above warn → proceed with warning.
|
|
127
220
|
if (concurrentSessions > T['concurrent-sessions-warn']) {
|
|
128
221
|
return {
|
|
129
222
|
decision: 'proceed',
|
|
130
223
|
agents: plannedAgents,
|
|
131
|
-
reasons: [
|
|
224
|
+
reasons: [
|
|
225
|
+
...(cpuTransientNote ? [cpuTransientNote] : []),
|
|
226
|
+
`warn: ${concurrentSessions} concurrent sessions`,
|
|
227
|
+
],
|
|
132
228
|
measurements,
|
|
133
229
|
};
|
|
134
230
|
}
|
|
@@ -137,7 +233,10 @@ function applyDecisionRules(measurements, opts) {
|
|
|
137
233
|
return {
|
|
138
234
|
decision: 'proceed',
|
|
139
235
|
agents: plannedAgents,
|
|
140
|
-
reasons: [
|
|
236
|
+
reasons: [
|
|
237
|
+
...(cpuTransientNote ? [cpuTransientNote] : []),
|
|
238
|
+
'all thresholds within bounds',
|
|
239
|
+
],
|
|
141
240
|
measurements,
|
|
142
241
|
};
|
|
143
242
|
}
|
|
@@ -148,14 +247,19 @@ function applyDecisionRules(measurements, opts) {
|
|
|
148
247
|
* @param {object} opts.config - Parsed Session Config (from parse-config.sh output)
|
|
149
248
|
* @param {number} opts.plannedAgents - Number of agents the session-plan wants to dispatch
|
|
150
249
|
* @param {string} opts.waveRole - e.g. "Impl-Core", "Quality"
|
|
151
|
-
* @param {object} [opts.probeOverride] - {ramFreeGb, cpuLoadPct, concurrentSessions}
|
|
152
|
-
* testing; when omitted, calls resource-probe
|
|
250
|
+
* @param {object} [opts.probeOverride] - {ramFreeGb, cpuLoadPct, cpuLoad5mPct?, concurrentSessions}
|
|
251
|
+
* for testing; when omitted, calls resource-probe
|
|
153
252
|
* @returns {Promise<{decision: "proceed"|"reduce"|"coordinator-direct", agents: number, reasons: string[], measurements: object}>}
|
|
154
253
|
*/
|
|
155
254
|
export async function evaluateWaveResourceGate(opts) {
|
|
156
255
|
const { config, plannedAgents } = opts;
|
|
157
256
|
|
|
158
257
|
// Rule 1: resource-awareness disabled — skip all probing.
|
|
258
|
+
// `resource-awareness: false` is a FULL opt-out, INCLUDING the HR-004
|
|
259
|
+
// heavy-repo static cap below (applyHeavyRepoCap runs only inside
|
|
260
|
+
// applyDecisionRules, which this early return bypasses entirely). The
|
|
261
|
+
// static cap only applies on the resource-aware path — deliberate,
|
|
262
|
+
// reviewed 2026-07-23, baseline #60.
|
|
159
263
|
if (config['resource-awareness'] === false) {
|
|
160
264
|
return {
|
|
161
265
|
decision: 'proceed',
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* worktree/listing.mjs — list and filter worktrees.
|
|
3
3
|
*
|
|
4
4
|
* Exports:
|
|
5
|
-
* listWorktrees() — list all git worktrees
|
|
5
|
+
* listWorktrees() — list all git worktrees (bare array; swallows git failure)
|
|
6
|
+
* listWorktreesChecked() — same listing WITH a "git actually ran" signal (#919.3)
|
|
6
7
|
* applyWorktreeExcludes(wtPath, patterns) — remove top-level dirs from worktree
|
|
7
8
|
*
|
|
8
9
|
* No imports from lifecycle.mjs — this module is intentionally a leaf of the
|
|
@@ -18,23 +19,39 @@ import path from 'node:path';
|
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
|
-
* List all git worktrees
|
|
22
|
+
* List all git worktrees WITH an explicit "git actually ran" signal (#919.3).
|
|
23
|
+
*
|
|
24
|
+
* The bare `listWorktrees()` below swallows a failing `git worktree list` into
|
|
25
|
+
* an empty array — indistinguishable from "git ran, repo has no extra
|
|
26
|
+
* worktrees". That collapse is the last fail-open gap in the
|
|
27
|
+
* `checkLiveForeignSession` full path (peer-discovery.mjs, #906/#908 residual):
|
|
28
|
+
* a total surface failure read as "nobody home". This variant keeps the listing
|
|
29
|
+
* contract but makes the failure DISTINGUISHABLE, so callers that need the
|
|
30
|
+
* distinction can make the fail-safe call themselves.
|
|
22
31
|
*
|
|
23
32
|
* @param {object} [opts]
|
|
24
33
|
* @param {Function} [opts.$] Optional zx-compatible executor. Defaults to real zx.$.
|
|
25
34
|
* Tests pass a mock here to avoid vi.mock('zx') under fork pool.
|
|
26
|
-
* @returns {Promise<Array<{path: string, branch: string, head: string}
|
|
27
|
-
*
|
|
35
|
+
* @returns {Promise<{ok: boolean, worktrees: Array<{path: string, branch: string, head: string}>, error?: string}>}
|
|
36
|
+
* `ok: true` — `git worktree list` ran; `worktrees` is the (possibly empty)
|
|
37
|
+
* parsed listing. An empty list here is a MEASUREMENT.
|
|
38
|
+
* `ok: false` — the git invocation itself failed (spawn error, non-zero
|
|
39
|
+
* exit, not a repo, …); `worktrees` is `[]` and `error` carries
|
|
40
|
+
* the failure message. An empty list here is NOT a measurement.
|
|
28
41
|
*/
|
|
29
|
-
export async function
|
|
42
|
+
export async function listWorktreesChecked(opts = {}) {
|
|
30
43
|
const dollar = opts.$ ?? defaultDollar;
|
|
31
44
|
const git = dollar({ cwd: process.cwd() });
|
|
32
45
|
let output;
|
|
33
46
|
try {
|
|
34
47
|
const result = await git`git worktree list --porcelain`;
|
|
35
48
|
output = result.stdout;
|
|
36
|
-
} catch {
|
|
37
|
-
return
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
worktrees: [],
|
|
53
|
+
error: err instanceof Error ? err.message : String(err),
|
|
54
|
+
};
|
|
38
55
|
}
|
|
39
56
|
|
|
40
57
|
const worktrees = [];
|
|
@@ -67,6 +84,26 @@ export async function listWorktrees(opts = {}) {
|
|
|
67
84
|
worktrees.push(current);
|
|
68
85
|
}
|
|
69
86
|
|
|
87
|
+
return { ok: true, worktrees };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* List all git worktrees in the current repository.
|
|
92
|
+
*
|
|
93
|
+
* Thin backward-compatible wrapper over `listWorktreesChecked()`: existing
|
|
94
|
+
* callers get the bare array they always got, INCLUDING the historical
|
|
95
|
+
* swallow-to-`[]` on git failure. Callers that must distinguish "git ran,
|
|
96
|
+
* empty" from "git failed" use `listWorktreesChecked()` instead (#919.3).
|
|
97
|
+
*
|
|
98
|
+
* @param {object} [opts]
|
|
99
|
+
* @param {Function} [opts.$] Optional zx-compatible executor. Defaults to real zx.$.
|
|
100
|
+
* Tests pass a mock here to avoid vi.mock('zx') under fork pool.
|
|
101
|
+
* @returns {Promise<Array<{path: string, branch: string, head: string}>>}
|
|
102
|
+
* Array of worktree descriptors; empty array if none OR on git failure
|
|
103
|
+
* (the two are indistinguishable here — by design, see wrapper note).
|
|
104
|
+
*/
|
|
105
|
+
export async function listWorktrees(opts = {}) {
|
|
106
|
+
const { worktrees } = await listWorktreesChecked(opts);
|
|
70
107
|
return worktrees;
|
|
71
108
|
}
|
|
72
109
|
|
package/scripts/mcp-server.sh
CHANGED
|
@@ -128,9 +128,23 @@ tool_session_config() {
|
|
|
128
128
|
return
|
|
129
129
|
fi
|
|
130
130
|
|
|
131
|
-
# Extract everything from "## Session Config" to the next heading or EOF
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
# Extract everything from "## Session Config" to the next heading or EOF.
|
|
132
|
+
#
|
|
133
|
+
# The address matches isSessionConfigHeading() in
|
|
134
|
+
# scripts/lib/config/section-extractor.mjs (the SSOT) exactly: the literal
|
|
135
|
+
# heading, plus an optional CR so a CRLF checkout is read the same way. Shell
|
|
136
|
+
# cannot import the JS predicate, so the alignment is by hand and this
|
|
137
|
+
# comment names the authority. Before #968 this was the only STRICTER site in
|
|
138
|
+
# the repo — a bare `$` anchors before the CR in a BRE, so on a CRLF checkout
|
|
139
|
+
# (this plugin ships to Windows-side Codex/Cursor users) the tool reported
|
|
140
|
+
# "no Session Config section" for a file the runtime parses fine.
|
|
141
|
+
#
|
|
142
|
+
# `\r` is not portable inside a BRE (GNU sed only), so the CR is injected as
|
|
143
|
+
# a literal byte via printf.
|
|
144
|
+
local cr config sc_re
|
|
145
|
+
cr=$(printf '\r')
|
|
146
|
+
sc_re="^## Session Config${cr}\{0,1\}\$"
|
|
147
|
+
config=$(sed -n "/$sc_re/,/^## /{/$sc_re/d;/^## /d;p;}" "$instr_file" 2>/dev/null) || true
|
|
134
148
|
|
|
135
149
|
if [[ -z "$config" ]]; then
|
|
136
150
|
respond "$id" "$(text_content "No '## Session Config' section found in $instr_file")"
|