session-orchestrator 3.20.0 → 3.22.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/000-session-orchestrator.mdc +3 -2
- package/.cursor/rules/030-wave-execution.mdc +10 -8
- package/.cursor/rules/040-discovery.mdc +6 -6
- package/.cursor/rules/050-plan.mdc +8 -8
- package/CHANGELOG.md +515 -0
- package/README.md +16 -11
- package/agents/analyst.md +1 -1
- package/agents/architect-reviewer.md +1 -1
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +1 -1
- package/agents/dialectic-deriver.md +1 -1
- package/agents/docs-writer.md +1 -1
- package/agents/memory-proposal-collector.md +7 -5
- package/agents/qa-strategist.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +42 -1
- package/agents/skill-applied-judge.md +1 -1
- package/agents/test-writer.md +1 -1
- package/agents/ui-developer.md +1 -1
- package/agents/ux-evaluator.md +1 -1
- package/commands/eli5.md +33 -0
- package/commands/release.md +62 -0
- package/commands/test.md +2 -2
- package/docs/components.md +6 -5
- package/docs/migration-v3.md +9 -6
- package/docs/persona-panel.md +3 -1
- package/docs/scope-collision-guard.md +167 -0
- package/docs/session-config-reference.md +31 -8
- package/hooks/_lib/lock-bootstrap.mjs +19 -13
- package/hooks/enforce-scope.mjs +103 -3
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +21 -1
- package/hooks/on-session-end.mjs +76 -97
- package/hooks/on-session-start.mjs +195 -104
- package/hooks/on-stop.mjs +127 -12
- package/hooks/post-bash-write-verify.mjs +8 -32
- package/hooks/pre-auq-clarity.mjs +787 -0
- package/hooks/pre-bash-issue-budget.mjs +17 -18
- package/hooks/pre-task-scope-disjoint.mjs +1042 -0
- package/package.json +3 -1
- package/pi/prompts/eli5.md +12 -0
- package/pi/prompts/release.md +12 -0
- package/scripts/auq-audit.mjs +825 -0
- package/scripts/autopilot.mjs +10 -9
- package/scripts/emit-session.mjs +42 -0
- package/scripts/export-hw-learnings.mjs +61 -2
- package/scripts/lib/auq/clarity.mjs +1314 -0
- package/scripts/lib/auq/parse.mjs +1006 -0
- package/scripts/lib/auq/schema.mjs +1457 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/backlog-scan.mjs +106 -15
- package/scripts/lib/build-live-signals.mjs +7 -3
- package/scripts/lib/ci-status-banner.mjs +267 -77
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
- package/scripts/lib/config/vault-integration.mjs +12 -1
- package/scripts/lib/dispatcher/rank.mjs +4 -7
- package/scripts/lib/gates/gate-full.mjs +3 -3
- package/scripts/lib/gates/gate-helpers.mjs +17 -6
- package/scripts/lib/git-config-drift.mjs +471 -0
- package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
- package/scripts/lib/io.mjs +432 -7
- package/scripts/lib/issue-budget.mjs +63 -9
- package/scripts/lib/learnings/select.mjs +157 -3
- package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
- package/scripts/lib/mirror-issues-banner.mjs +266 -0
- package/scripts/lib/named-vault-resolver.mjs +105 -16
- package/scripts/lib/owner-interview.mjs +78 -32
- package/scripts/lib/peer-cards/schema.mjs +6 -2
- package/scripts/lib/peer-discovery.mjs +73 -22
- package/scripts/lib/project-hygiene.mjs +64 -4
- package/scripts/lib/reconcile/renderer.mjs +17 -4
- package/scripts/lib/reconcile/writer.mjs +69 -30
- package/scripts/lib/redact-spans.mjs +89 -0
- package/scripts/lib/resource-probe/evaluate.mjs +330 -149
- package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
- package/scripts/lib/resource-probe.mjs +18 -2
- package/scripts/lib/scope-baseline.mjs +77 -17
- package/scripts/lib/scope-gate.mjs +658 -0
- package/scripts/lib/secret-masker.mjs +262 -0
- package/scripts/lib/session-lock.mjs +34 -10
- package/scripts/lib/session-registry.mjs +9 -1
- package/scripts/lib/spiral-carryover.mjs +23 -2
- package/scripts/lib/state-md/mission-status.mjs +164 -58
- package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
- package/scripts/lib/validate/check-agents.mjs +77 -5
- package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
- package/scripts/lib/validate/check-commands.mjs +2 -20
- package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +48 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +185 -17
- package/scripts/lib/validate/check-rules.mjs +153 -9
- package/scripts/lib/validate/check-skills.mjs +191 -0
- package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
- package/scripts/lib/validate/check-unwired-features.mjs +219 -11
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
- package/scripts/lib/validate/frontmatter-block.mjs +61 -0
- package/scripts/lib/validate/tier-inference.mjs +46 -8
- package/scripts/lib/vault-backfill/glab.mjs +91 -58
- package/scripts/lib/vault-backfill/manifest.mjs +28 -8
- package/scripts/lib/vault-mirror/namespace.mjs +146 -1
- package/scripts/lib/vault-mirror/process.mjs +264 -31
- package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
- package/scripts/lib/vault-status/board-writer.mjs +300 -56
- package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
- package/scripts/lib/vcs-repo-spec.mjs +680 -30
- package/scripts/lib/wave-resource-gate.mjs +67 -73
- package/scripts/materialize-wave-scope.mjs +281 -0
- package/scripts/print-learnings-index.mjs +30 -3
- package/scripts/release.mjs +983 -107
- package/scripts/run-quality-gate.mjs +14 -0
- package/scripts/site-numbers.mjs +1049 -0
- package/scripts/validate-plugin.mjs +64 -0
- package/scripts/validate-wave-scope.mjs +286 -12
- package/scripts/vault-backfill.mjs +32 -5
- package/scripts/vault-mirror.mjs +26 -1
- package/skills/_shared/monitor-patterns.md +24 -4
- package/skills/_shared/parallel-aware-auq.md +30 -24
- package/skills/_shared/parallel-aware-preamble.md +31 -2
- package/skills/_shared/state-ownership.md +49 -6
- package/skills/bootstrap/SKILL.md +2 -1
- package/skills/brainstorm/SKILL.md +18 -18
- package/skills/brainstorm/soul.md +12 -0
- package/skills/claude-md-drift-check/SKILL.md +9 -1
- package/skills/debug/SKILL.md +4 -1
- package/skills/discovery/SKILL.md +28 -24
- package/skills/discovery/issue-templates.md +4 -4
- package/skills/discovery/probes-code.md +2 -2
- package/skills/discovery/probes-feature.md +6 -6
- package/skills/discovery/probes-infra.md +2 -2
- package/skills/discovery/probes-session.md +5 -5
- package/skills/dispatcher/SKILL.md +10 -1
- package/skills/eli5/SKILL.md +43 -0
- package/skills/evolve/SKILL.md +8 -9
- package/skills/frontmatter-guard/SKILL.md +9 -1
- package/skills/gitlab-ops/SKILL.md +73 -59
- package/skills/gitlab-portfolio/SKILL.md +10 -1
- package/skills/grill/SKILL.md +6 -6
- package/skills/grill/soul.md +16 -0
- package/skills/memory-cleanup/SKILL.md +20 -7
- package/skills/npm-publish/SKILL.md +23 -51
- package/skills/peekaboo-driver/SKILL.md +3 -3
- package/skills/persona-panel/SKILL.md +3 -1
- package/skills/plan/SKILL.md +18 -16
- package/skills/plan/mode-feature.md +1 -1
- package/skills/plan/mode-new.md +42 -12
- package/skills/plan/soul.md +12 -0
- package/skills/reconcile/SKILL.md +3 -3
- package/skills/repo-audit/SKILL.md +10 -1
- package/skills/session-end/SKILL.md +97 -22
- package/skills/session-end/metrics-collection.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +37 -2
- package/skills/session-end/session-metrics-write.md +4 -10
- package/skills/session-plan/SKILL.md +2 -2
- package/skills/session-plan/wave-template.md +1 -1
- package/skills/session-start/SKILL.md +82 -36
- package/skills/session-start/phase-2-5-docs-planning.md +8 -8
- package/skills/session-start/phase-4-5-resource-health.md +82 -19
- package/skills/session-start/soul.md +110 -0
- package/skills/spinout/SKILL.md +5 -1
- package/skills/sunset-review/SKILL.md +11 -1
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/tmux-layout/SKILL.md +7 -2
- package/skills/using-orchestrator/SKILL.md +1 -1
- package/skills/vault-mirror/SKILL.md +10 -1
- package/skills/vault-sync/SKILL.md +10 -1
- package/skills/vault-sync/validator.mjs +55 -6
- package/skills/wave-executor/wave-loop.md +64 -12
- package/skills/write-executable-plan/SKILL.md +6 -6
- package/scripts/lib/mission-status-schema.mjs +0 -114
- package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
- package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
- package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
|
@@ -7,12 +7,22 @@
|
|
|
7
7
|
* no-op condition (no VCS, CLI missing, timeout, parse failure).
|
|
8
8
|
*
|
|
9
9
|
* Supports GitLab (via glab) and GitHub (via gh).
|
|
10
|
-
* VCS is auto-detected from
|
|
10
|
+
* VCS is auto-detected from the repo's git remotes via
|
|
11
|
+
* `vcs-repo-spec.mjs`'s `detectVcsFamily` — NOT from a hard-coded `origin`
|
|
12
|
+
* lookup, which left the banner structurally dark in every repo whose remotes
|
|
13
|
+
* are named `gitlab`/`github` (#1039).
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
16
|
import { execFile as _execFile } from 'node:child_process';
|
|
14
17
|
import { promisify } from 'node:util';
|
|
15
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
resolveRepoSpec,
|
|
20
|
+
resolveRepoHost,
|
|
21
|
+
resolveGitlabProjectTarget,
|
|
22
|
+
redactUrlCredentials,
|
|
23
|
+
detectVcsFamily,
|
|
24
|
+
isQueryFailure,
|
|
25
|
+
} from './vcs-repo-spec.mjs';
|
|
16
26
|
|
|
17
27
|
const execFileAsync = promisify(_execFile);
|
|
18
28
|
|
|
@@ -38,24 +48,130 @@ async function execWithTimeout(cmd, args, opts = {}) {
|
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
51
|
+
* The one failure this probe can produce that the frozen
|
|
52
|
+
* `REMOTE_RESOLUTION_REASONS` set has no member for: the async timeout race
|
|
53
|
+
* every subprocess here runs under has no counterpart in the SYNCHRONOUS
|
|
54
|
+
* `vcs-repo-spec.mjs` core (it uses `execFileSync`, which cannot time out).
|
|
55
|
+
*
|
|
56
|
+
* Deliberately NOT a query-failure for warning purposes — a timeout stays
|
|
57
|
+
* SILENT, matching how `checkCiStatus`'s outer catch has always treated one
|
|
58
|
+
* (`msg === 'timeout'` → silent `null`). Warning on a hung `git` but not on a
|
|
59
|
+
* hung `glab` would be an inconsistency inside a single banner, and a hang is
|
|
60
|
+
* not a fact an operator can act on the way "git is not installed" is.
|
|
61
|
+
*/
|
|
62
|
+
const PROBE_TIMEOUT = 'probe-timeout';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The ONE `isQueryFailure` member this banner deliberately keeps silent.
|
|
66
|
+
*
|
|
67
|
+
* `vcs-repo-spec.mjs` classifies `not-a-git-repo` as a query failure, and that
|
|
68
|
+
* is correct IN ITS DOMAIN: a remote-resolution helper genuinely could not
|
|
69
|
+
* enumerate remotes. For THIS consumer the same fact is a benign absence —
|
|
70
|
+
* session-start Phase 4 asking a non-repo directory about its CI has a complete
|
|
71
|
+
* and unsurprising answer ("there is no CI here"), and an operator cannot act on
|
|
72
|
+
* being told their directory is not a git repo. Warning would print on every
|
|
73
|
+
* such session-start and train exactly the reflex that makes the
|
|
74
|
+
* `git-unavailable` / `git-error` lines worthless.
|
|
75
|
+
*
|
|
76
|
+
* Expressed as a SUBTRACTION from `isQueryFailure` rather than as a hard-coded
|
|
77
|
+
* warn-list, so a reason added to the frozen set upstream is warned by default
|
|
78
|
+
* (fail-toward-visible) instead of silently inheriting the benign path.
|
|
79
|
+
*
|
|
80
|
+
* @type {ReadonlySet<string>}
|
|
81
|
+
*/
|
|
82
|
+
const SILENT_QUERY_FAILURES = new Set(['not-a-git-repo']);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Classify a rejected ASYNC `execFile` into a `REMOTE_RESOLUTION_REASONS`
|
|
86
|
+
* member.
|
|
87
|
+
*
|
|
88
|
+
* This cannot reuse `vcs-repo-spec.mjs`'s own (unexported) `classifyGitFailure`
|
|
89
|
+
* because that one reads an `execFileSync` result, where the spawn errno lands
|
|
90
|
+
* in `err.code` and the child's exit status in `err.status`. Async `execFile`
|
|
91
|
+
* folds BOTH into `err.code`, discriminated only by TYPE: the string `'ENOENT'`
|
|
92
|
+
* when the binary is not on PATH, the number `128` when git ran and rejected
|
|
93
|
+
* the path as a work tree. Same taxonomy, different carrier.
|
|
94
|
+
*
|
|
95
|
+
* @param {unknown} err
|
|
96
|
+
* @returns {'git-unavailable'|'not-a-git-repo'|'git-error'}
|
|
97
|
+
*/
|
|
98
|
+
function classifyGitProbeFailure(err) {
|
|
99
|
+
const code = err && typeof err === 'object' ? /** @type {any} */ (err).code : undefined;
|
|
100
|
+
if (code === 'ENOENT') return 'git-unavailable';
|
|
101
|
+
// `git remote -v` exits 128 for "not a git repository" — and, unlike the
|
|
102
|
+
// `remote get-url origin` call this replaced, NOT for "no such remote":
|
|
103
|
+
// a repo with zero remotes exits 0 with empty stdout. That is exactly the
|
|
104
|
+
// split that makes `no-remotes` (benign absence) reportable at all.
|
|
105
|
+
if (code === 128) return 'not-a-git-repo';
|
|
106
|
+
return 'git-error';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Detect the repo's VCS family from its git remotes.
|
|
111
|
+
*
|
|
112
|
+
* **Never throws.** Returns a discriminated result carrying a reason from
|
|
113
|
+
* `vcs-repo-spec.mjs`'s frozen `REMOTE_RESOLUTION_REASONS` (plus
|
|
114
|
+
* {@link PROBE_TIMEOUT}), so the caller can separate "I could not ask"
|
|
115
|
+
* (`isQueryFailure` → WARN) from "I asked; this repo has no VCS remote"
|
|
116
|
+
* (benign, silent). This is the `mirror-issues-banner.mjs` shape — a third
|
|
117
|
+
* state beyond `value | null` — applied to the last unmigrated path in this
|
|
118
|
+
* module (#1039).
|
|
119
|
+
*
|
|
120
|
+
* What it replaces, and why both halves were defects:
|
|
121
|
+
* 1. `git remote get-url origin` — a hard-coded remote NAME. In a repo whose
|
|
122
|
+
* remotes are `gitlab` + `github` (no `origin`) the call failed, the
|
|
123
|
+
* caller swallowed it to `null`, and the banner was structurally dark:
|
|
124
|
+
* it could never report, not even on a red pipeline.
|
|
125
|
+
* 2. `remoteUrl.includes('github.com') ? 'github' : 'gitlab'` — a substring
|
|
126
|
+
* test that classifies GitHub Enterprise (`git@github.example.com:o/r`)
|
|
127
|
+
* as gitlab and then drives `glab` at a GitHub instance.
|
|
128
|
+
* `detectVcsFamily` fixes both: preference-ordered remote selection, and
|
|
129
|
+
* host-prefix classification (`github.*` / `gitlab.*`) ahead of remote-name.
|
|
130
|
+
*
|
|
131
|
+
* The `git remote -v` spawn stays HERE rather than inside `detectVcsFamily`
|
|
132
|
+
* because this module owes every subprocess a timeout race — the shared core
|
|
133
|
+
* is synchronous by design. So the output is captured once and handed to the
|
|
134
|
+
* shared classifier through its `gitRun` seam: one spawn, one taxonomy, no
|
|
135
|
+
* second copy of the parsing or the credential strip.
|
|
43
136
|
*
|
|
44
137
|
* @param {string} repoRoot
|
|
45
138
|
* @param {{ execFile?: Function, timeoutMs?: number }} deps
|
|
46
|
-
* @returns {Promise<'github'|'gitlab'
|
|
139
|
+
* @returns {Promise<{ ok: true, vcs: 'github'|'gitlab' }
|
|
140
|
+
* | { ok: false, reason: string, stderr?: string }>}
|
|
47
141
|
*/
|
|
48
142
|
async function detectVcs(repoRoot, deps = {}) {
|
|
49
143
|
// Use the smaller of 2000ms or the caller-supplied timeout so that a short
|
|
50
144
|
// test-level timeout is still respected here.
|
|
51
145
|
const gitTimeout = Math.min(2000, deps.timeoutMs ?? 2000);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
146
|
+
|
|
147
|
+
let stdout;
|
|
148
|
+
try {
|
|
149
|
+
const result = await execWithTimeout(
|
|
150
|
+
'git',
|
|
151
|
+
['remote', '-v'],
|
|
152
|
+
{ cwd: repoRoot, timeoutMs: gitTimeout, execFile: deps.execFile },
|
|
153
|
+
);
|
|
154
|
+
stdout = String(result?.stdout ?? '');
|
|
155
|
+
} catch (err) {
|
|
156
|
+
const msg = err instanceof Error ? err.message : String(err ?? '');
|
|
157
|
+
if (msg === 'timeout') return { ok: false, reason: PROBE_TIMEOUT };
|
|
158
|
+
const stderr = err && typeof err === 'object' ? String(/** @type {any} */ (err).stderr ?? '') : '';
|
|
159
|
+
return { ok: false, reason: classifyGitProbeFailure(err), stderr };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Replay the captured output through the shared core's `gitRun` seam. The
|
|
163
|
+
// arg guard is insurance, not decoration: `detectVcsFamily` asks only for
|
|
164
|
+
// `remote -v` today, and if it ever grows a second query the guard turns a
|
|
165
|
+
// silently-wrong answer (built from the WRONG command's output) into a
|
|
166
|
+
// reported `git-error`.
|
|
167
|
+
const replayGitRun = (args) =>
|
|
168
|
+
Array.isArray(args) && args.at(-2) === 'remote' && args.at(-1) === '-v'
|
|
169
|
+
? { ok: true, stdout, stderr: '', status: 0 }
|
|
170
|
+
: { ok: false, stdout: '', stderr: `unsupported git query in ci-status probe: ${String(args)}`, status: 1 };
|
|
171
|
+
|
|
172
|
+
const family = detectVcsFamily({ repoRoot, gitRun: replayGitRun });
|
|
173
|
+
if (!family.ok) return { ok: false, reason: family.reason, stderr: family.stderr };
|
|
174
|
+
return { ok: true, vcs: family.vcs };
|
|
59
175
|
}
|
|
60
176
|
|
|
61
177
|
/**
|
|
@@ -76,42 +192,19 @@ async function getHeadSha(repoRoot, deps = {}) {
|
|
|
76
192
|
}
|
|
77
193
|
|
|
78
194
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* #872: pins to `deps.repoSpec` via `-R` when resolved (host-pinning —
|
|
82
|
-
* `glab repo view` otherwise falls back to the ambient `GITLAB_HOST`).
|
|
83
|
-
*
|
|
84
|
-
* @param {string} repoRoot
|
|
85
|
-
* @param {{ execFile?: Function, timeoutMs?: number, repoSpec?: string }} deps
|
|
86
|
-
* @returns {Promise<number>}
|
|
87
|
-
*/
|
|
88
|
-
async function getGlabProjectId(repoRoot, deps = {}) {
|
|
89
|
-
const args = ['repo', 'view', '--output', 'json'];
|
|
90
|
-
if (deps.repoSpec) args.push('-R', deps.repoSpec);
|
|
91
|
-
const result = await execWithTimeout(
|
|
92
|
-
'glab',
|
|
93
|
-
args,
|
|
94
|
-
{ cwd: repoRoot, timeoutMs: deps.timeoutMs ?? DEFAULT_TIMEOUT_MS, execFile: deps.execFile },
|
|
95
|
-
);
|
|
96
|
-
const parsed = JSON.parse(result.stdout);
|
|
97
|
-
return parsed.id;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Run `glab api <path>` and return parsed JSON.
|
|
195
|
+
* Run a host-pinned `glab api <path>` request and return parsed JSON.
|
|
102
196
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* `
|
|
197
|
+
* GitLab's API accepts neither a remote URL nor `-R`; callers provide the host
|
|
198
|
+
* proven by `resolveGitlabProjectTarget` so this helper cannot fall back to
|
|
199
|
+
* ambient `GITLAB_HOST` configuration.
|
|
106
200
|
*
|
|
107
201
|
* @param {string} apiPath
|
|
108
202
|
* @param {string} repoRoot
|
|
109
|
-
* @param {{ execFile?: Function, timeoutMs?: number, repoHost
|
|
203
|
+
* @param {{ execFile?: Function, timeoutMs?: number, repoHost: string }} deps
|
|
110
204
|
* @returns {Promise<unknown>}
|
|
111
205
|
*/
|
|
112
206
|
async function glabApi(apiPath, repoRoot, deps = {}) {
|
|
113
|
-
const args = ['api', apiPath];
|
|
114
|
-
if (deps.repoHost) args.push('--hostname', deps.repoHost);
|
|
207
|
+
const args = ['api', apiPath, '--hostname', deps.repoHost];
|
|
115
208
|
const result = await execWithTimeout(
|
|
116
209
|
'glab',
|
|
117
210
|
args,
|
|
@@ -161,17 +254,32 @@ function ageDaysFrom(isoDate, now) {
|
|
|
161
254
|
*
|
|
162
255
|
* @param {string} repoRoot
|
|
163
256
|
* @param {number} now
|
|
164
|
-
* @param {{
|
|
257
|
+
* @param {{
|
|
258
|
+
* execFile?: Function,
|
|
259
|
+
* timeoutMs?: number,
|
|
260
|
+
* gitlabProject?: { host: string, encodedProjectPath: string },
|
|
261
|
+
* }} deps
|
|
165
262
|
* @returns {Promise<object|null>}
|
|
166
263
|
*/
|
|
167
264
|
async function checkGitlab(repoRoot, now, deps = {}) {
|
|
168
|
-
const
|
|
169
|
-
|
|
265
|
+
const project = deps.gitlabProject;
|
|
266
|
+
if (
|
|
267
|
+
!project ||
|
|
268
|
+
typeof project.host !== 'string' ||
|
|
269
|
+
typeof project.encodedProjectPath !== 'string' ||
|
|
270
|
+
project.host === '' ||
|
|
271
|
+
project.encodedProjectPath === ''
|
|
272
|
+
) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
170
275
|
|
|
276
|
+
const currentSha = await getHeadSha(repoRoot, deps);
|
|
277
|
+
const apiDeps = { ...deps, repoHost: project.host };
|
|
278
|
+
const projectPath = `projects/${project.encodedProjectPath}`;
|
|
171
279
|
const pipelines = await glabApi(
|
|
172
|
-
|
|
280
|
+
`${projectPath}/pipelines?order_by=updated_at&sort=desc&per_page=15`,
|
|
173
281
|
repoRoot,
|
|
174
|
-
|
|
282
|
+
apiDeps,
|
|
175
283
|
);
|
|
176
284
|
|
|
177
285
|
if (!Array.isArray(pipelines)) return null;
|
|
@@ -201,9 +309,9 @@ async function checkGitlab(repoRoot, now, deps = {}) {
|
|
|
201
309
|
let allowFailureJobs;
|
|
202
310
|
try {
|
|
203
311
|
const jobs = await glabApi(
|
|
204
|
-
|
|
312
|
+
`${projectPath}/pipelines/${currentPipeline.id}/jobs`,
|
|
205
313
|
repoRoot,
|
|
206
|
-
|
|
314
|
+
apiDeps,
|
|
207
315
|
);
|
|
208
316
|
if (Array.isArray(jobs)) {
|
|
209
317
|
const softFailed = jobs
|
|
@@ -268,9 +376,9 @@ async function checkGitlab(repoRoot, now, deps = {}) {
|
|
|
268
376
|
let failingJobName;
|
|
269
377
|
try {
|
|
270
378
|
const jobs = await glabApi(
|
|
271
|
-
|
|
379
|
+
`${projectPath}/pipelines/${currentPipeline.id}/jobs`,
|
|
272
380
|
repoRoot,
|
|
273
|
-
|
|
381
|
+
apiDeps,
|
|
274
382
|
);
|
|
275
383
|
if (Array.isArray(jobs)) {
|
|
276
384
|
const failedJob = jobs.find((j) => j.status === 'failed');
|
|
@@ -308,9 +416,25 @@ async function checkGitlab(repoRoot, now, deps = {}) {
|
|
|
308
416
|
/**
|
|
309
417
|
* GitHub CI status check (v1 — red/green only; lastGreen not implemented).
|
|
310
418
|
*
|
|
311
|
-
* #872: pins the `gh repo view` lookup to `deps.repoSpec`
|
|
312
|
-
*
|
|
313
|
-
*
|
|
419
|
+
* #872/#1022: pins the `gh repo view` lookup to `deps.repoSpec` when resolved
|
|
420
|
+
* (host-pinning — `gh repo view` otherwise falls back to the ambient
|
|
421
|
+
* `GH_HOST`). The spec is passed POSITIONALLY, not via `-R`: `gh repo view`
|
|
422
|
+
* takes `[<repository>]` as a positional argument and has no `-R`/`--repo`
|
|
423
|
+
* flag at all, so `-R` made gh exit 1 with `unknown shorthand flag: 'R'` —
|
|
424
|
+
* an error `checkCiStatus`'s outer catch swallowed to `null`, leaving the
|
|
425
|
+
* Phase 4 banner silently dead on EVERY GitHub repo (#1022). The
|
|
426
|
+
* `[HOST/]OWNER/REPO` shape `resolveRepoSpec({ vcs: 'github' })` returns is
|
|
427
|
+
* exactly the positional's documented input format.
|
|
428
|
+
*
|
|
429
|
+
* GitLab CI differs deliberately: it derives a host-pinned API target from
|
|
430
|
+
* the selected remote, while GitHub still needs this lookup because its API
|
|
431
|
+
* path requires `nameWithOwner`. Do not unify these call sites.
|
|
432
|
+
*
|
|
433
|
+
* `nameWithOwner` is NOT derivable from `deps.repoSpec`, so this lookup
|
|
434
|
+
* cannot be dropped: the spec carries a HOST prefix the `repos/<owner>/<repo>`
|
|
435
|
+
* API path must not contain, it is `undefined` whenever no remote resolves
|
|
436
|
+
* (cross-family guard, unsafe-argv guard), and `normalizeGithubSpec` falls
|
|
437
|
+
* back to the raw URL on an unrecognised remote shape.
|
|
314
438
|
*
|
|
315
439
|
* @param {string} repoRoot
|
|
316
440
|
* @param {{ execFile?: Function, timeoutMs?: number, repoSpec?: string, repoHost?: string }} deps
|
|
@@ -318,8 +442,9 @@ async function checkGitlab(repoRoot, now, deps = {}) {
|
|
|
318
442
|
*/
|
|
319
443
|
async function checkGithub(repoRoot, deps = {}) {
|
|
320
444
|
// Resolve owner/repo from gh to keep the API path generic.
|
|
321
|
-
const repoViewArgs = ['repo', 'view'
|
|
322
|
-
if (deps.repoSpec) repoViewArgs.push(
|
|
445
|
+
const repoViewArgs = ['repo', 'view'];
|
|
446
|
+
if (deps.repoSpec) repoViewArgs.push(deps.repoSpec);
|
|
447
|
+
repoViewArgs.push('--json', 'nameWithOwner');
|
|
323
448
|
const repoViewResult = await execWithTimeout(
|
|
324
449
|
'gh',
|
|
325
450
|
repoViewArgs,
|
|
@@ -387,11 +512,17 @@ async function checkGithub(repoRoot, deps = {}) {
|
|
|
387
512
|
* Checks CI status for the current HEAD commit.
|
|
388
513
|
*
|
|
389
514
|
* Returns `null` (silent no-op) when:
|
|
390
|
-
* -
|
|
515
|
+
* - The repo has no usable VCS remote (not a git repo, no remotes at all,
|
|
516
|
+
* or >= 2 remotes with no preference match) — a benign, measured absence
|
|
391
517
|
* - Required CLI (glab / gh) not in PATH
|
|
392
518
|
* - Any CLI invocation times out
|
|
393
519
|
* - JSON parse failure on CLI output
|
|
394
520
|
*
|
|
521
|
+
* Also returns `null`, but with a `console.warn` trace, when the VCS-detection
|
|
522
|
+
* QUERY ITSELF failed (`git` not on PATH, `git remote -v` erroring) or when a
|
|
523
|
+
* present CLI rejected its invocation. `null` alone cannot express "could not
|
|
524
|
+
* read" — see the outer catch and Step 1 for why the warn channel carries it.
|
|
525
|
+
*
|
|
395
526
|
* @param {{
|
|
396
527
|
* repoRoot?: string,
|
|
397
528
|
* vcs?: 'gitlab'|'github',
|
|
@@ -402,10 +533,11 @@ async function checkGithub(repoRoot, deps = {}) {
|
|
|
402
533
|
* execFile?: Function,
|
|
403
534
|
* resolveRepoSpec?: (opts: { repoRoot: string, vcs: 'gitlab'|'github' }) => string|undefined,
|
|
404
535
|
* resolveRepoHost?: (opts: { repoRoot: string, vcs: 'gitlab'|'github' }) => string|undefined,
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
536
|
+
* resolveGitlabProjectTarget?: (opts: { repoRoot: string }) =>
|
|
537
|
+
* { host: string, encodedProjectPath: string }|undefined,
|
|
538
|
+
* }} deps Dependency-injection seam for testing. GitLab defaults to
|
|
539
|
+
* `resolveGitlabProjectTarget`, which proves host and project path from one
|
|
540
|
+
* sanitized remote; GitHub retains the #872 spec/host resolvers.
|
|
409
541
|
* @returns {Promise<null | {
|
|
410
542
|
* status: 'green'|'red'|'unknown',
|
|
411
543
|
* ok: boolean,
|
|
@@ -434,6 +566,7 @@ export async function checkCiStatus(opts = {}, deps = {}) {
|
|
|
434
566
|
|
|
435
567
|
const resolveRepoSpecDep = deps.resolveRepoSpec ?? resolveRepoSpec;
|
|
436
568
|
const resolveRepoHostDep = deps.resolveRepoHost ?? resolveRepoHost;
|
|
569
|
+
const resolveGitlabProjectTargetDep = deps.resolveGitlabProjectTarget ?? resolveGitlabProjectTarget;
|
|
437
570
|
|
|
438
571
|
const depsWithExec = { execFile: execFileDep, timeoutMs };
|
|
439
572
|
|
|
@@ -441,30 +574,59 @@ export async function checkCiStatus(opts = {}, deps = {}) {
|
|
|
441
574
|
// Step 1: detect VCS (or use forced value).
|
|
442
575
|
let vcs = forcedVcs;
|
|
443
576
|
if (!vcs) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
//
|
|
577
|
+
const detected = await detectVcs(repoRoot, depsWithExec);
|
|
578
|
+
if (!detected.ok) {
|
|
579
|
+
// The two-state `null` return is fixed by 13 sibling banners, so the
|
|
580
|
+
// third state lives in the WARN channel: an ABSENCE (`no-remotes`,
|
|
581
|
+
// `no-matching-remote`, `unsafe-value`) is a real, benign answer and
|
|
582
|
+
// stays silent — warning there would train operators to ignore this
|
|
583
|
+
// line, which is the more expensive error. A QUERY FAILURE
|
|
584
|
+
// (`git-unavailable`, `git-error`) means the question could not be
|
|
585
|
+
// asked at all, and that is precisely the state that was
|
|
586
|
+
// indistinguishable from "nothing to report" before #1039.
|
|
587
|
+
// `not-a-git-repo` is the one query failure this banner reads as an
|
|
588
|
+
// absence — see {@link SILENT_QUERY_FAILURES}.
|
|
589
|
+
if (isQueryFailure(detected.reason) && !SILENT_QUERY_FAILURES.has(detected.reason)) {
|
|
590
|
+
const detail = detected.stderr
|
|
591
|
+
? ` — ${redactUrlCredentials(detected.stderr).trim()}`
|
|
592
|
+
: '';
|
|
593
|
+
console.warn(
|
|
594
|
+
`WARN ci-status-banner: VCS detection failed (${detected.reason}), banner suppressed — ` +
|
|
595
|
+
`CI state is UNKNOWN, not "green".${detail}`,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
448
598
|
return null;
|
|
449
599
|
}
|
|
600
|
+
vcs = detected.vcs;
|
|
450
601
|
}
|
|
451
602
|
|
|
452
|
-
// Step 1b
|
|
453
|
-
//
|
|
454
|
-
// GITLAB_HOST/
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
//
|
|
603
|
+
// Step 1b: GitLab's API target must be proven before its first glab spawn.
|
|
604
|
+
// A missing target is never permission to fall back to ambient
|
|
605
|
+
// GITLAB_HOST/repository configuration. GitHub retains its distinct
|
|
606
|
+
// repository lookup because its API path requires `nameWithOwner`.
|
|
607
|
+
//
|
|
608
|
+
// It is also not an ABSENCE: detectVcs just proved a GitLab remote exists,
|
|
609
|
+
// so `!gitlabProject` means "remote present, its form was rejected" — a
|
|
610
|
+
// QUERY FAILURE by the same rule the block above states. Returning null
|
|
611
|
+
// silently put this repo back in the pre-#1039 state where "CI green" and
|
|
612
|
+
// "could not ask" look identical. Reachable inputs measured 2026-08-21:
|
|
613
|
+
// `git://` scheme, a doubled slash in the path, a query string.
|
|
462
614
|
if (vcs === 'gitlab') {
|
|
463
|
-
|
|
615
|
+
const gitlabProject = resolveGitlabProjectTargetDep({ repoRoot });
|
|
616
|
+
if (!gitlabProject) {
|
|
617
|
+
console.warn(
|
|
618
|
+
'WARN ci-status-banner: a GitLab remote was detected but its host/project path ' +
|
|
619
|
+
'could not be derived, banner suppressed — CI state is UNKNOWN, not "green".',
|
|
620
|
+
);
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
return await checkGitlab(repoRoot, now, { ...depsWithExec, gitlabProject });
|
|
464
624
|
}
|
|
465
625
|
|
|
466
626
|
if (vcs === 'github') {
|
|
467
|
-
|
|
627
|
+
const repoSpec = resolveRepoSpecDep({ repoRoot, vcs });
|
|
628
|
+
const repoHost = resolveRepoHostDep({ repoRoot, vcs });
|
|
629
|
+
return await checkGithub(repoRoot, { ...depsWithExec, repoSpec, repoHost });
|
|
468
630
|
}
|
|
469
631
|
|
|
470
632
|
// Unknown VCS value — silent no-op.
|
|
@@ -482,7 +644,35 @@ export async function checkCiStatus(opts = {}, deps = {}) {
|
|
|
482
644
|
return null;
|
|
483
645
|
}
|
|
484
646
|
|
|
485
|
-
//
|
|
647
|
+
// Everything else means the CLI was PRESENT but the invocation failed —
|
|
648
|
+
// non-zero exit, rejected flag, unparseable output. Returning a bare null
|
|
649
|
+
// here makes that state indistinguishable from "CLI not installed", which
|
|
650
|
+
// is exactly how #1022 (`gh repo view -R` → `unknown shorthand flag: 'R'`)
|
|
651
|
+
// stayed invisible on every GitHub repo. THIS module keeps the two-state
|
|
652
|
+
// contract (null ⇒ silent no-op) for backwards compatibility with the
|
|
653
|
+
// Phase-4 callers already written against it, so leave a stderr trace
|
|
654
|
+
// instead: non-blocking, but the next defect of this class is no longer
|
|
655
|
+
// silent. Credentials redacted defense-in-depth (#907) — the message can
|
|
656
|
+
// quote the failed argv, which carries the repo spec.
|
|
657
|
+
//
|
|
658
|
+
// DIRECTION FOR NEW BANNERS — do not copy this shape. The
|
|
659
|
+
// absence-preserving form is `scripts/lib/mirror-issues-banner.mjs`: a
|
|
660
|
+
// THIRD return state `{ severity, message, degraded }` whose `degraded`
|
|
661
|
+
// is a member of the closed `DEGRADED_REASONS` enum exported there, so
|
|
662
|
+
// "could not read" stays distinguishable from "read, and clean".
|
|
663
|
+
//
|
|
664
|
+
// The REMOTE probe (`detectVcs`) has since been pulled onto that shape
|
|
665
|
+
// (#1039): it returns a reason from the frozen `REMOTE_RESOLUTION_REASONS`
|
|
666
|
+
// set and Step 1 branches on `isQueryFailure` — absence silent, query
|
|
667
|
+
// failure warned. This outer catch is what remains unmigrated: it still
|
|
668
|
+
// collapses every CLI-side failure onto `null`, which a caller reads as
|
|
669
|
+
// all-clear, and it cannot be widened without changing a return contract
|
|
670
|
+
// 13 sibling banners share. Same verdict, stated caller-side, in
|
|
671
|
+
// `skills/session-start/SKILL.md` § Phase 4 (the mirror-issues
|
|
672
|
+
// paragraph): "Do not reproduce it."
|
|
673
|
+
console.warn(
|
|
674
|
+
`WARN ci-status-banner: CI status check failed, banner suppressed — ${redactUrlCredentials(msg)}`,
|
|
675
|
+
);
|
|
486
676
|
return null;
|
|
487
677
|
}
|
|
488
678
|
}
|
|
@@ -43,13 +43,36 @@ import { hasBlockHeader } from './block-header.mjs';
|
|
|
43
43
|
const ALLOWED_AUTONOMY = ['off', 'advisory', 'autonomous-gated'];
|
|
44
44
|
const DEFAULT_CONFIDENCE_FLOOR = 0.5;
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* The display-only tail of an option LABEL, stripped before the enum match.
|
|
48
|
+
*
|
|
49
|
+
* The label and the stored value were the SAME string, which made the label
|
|
50
|
+
* unchangeable: both consumers (`skills/session-start/SKILL.md` Phase 1.1 and
|
|
51
|
+
* `skills/bootstrap/SKILL.md` Phase 3.5.1) hand the SELECTED LABEL straight to
|
|
52
|
+
* writeDispatcherAutonomyBlock(), and coerceAutonomy() falls back to 'off' on
|
|
53
|
+
* any value outside the enum. Putting AUQ-003's `(Recommended)` marker into the
|
|
54
|
+
* label would therefore have written `autonomy: off` for an operator who picked
|
|
55
|
+
* `advisory` — a silent downgrade, fail-closed and therefore invisible.
|
|
56
|
+
*
|
|
57
|
+
* Measured before this split (renderDispatcherAutonomyBlock, 2026-08-22):
|
|
58
|
+
* 'advisory' => autonomy: advisory
|
|
59
|
+
* 'Advisory (surface only)' => autonomy: off ← the downgrade
|
|
60
|
+
*
|
|
61
|
+
* Anchored at the end, so it can only ever strip a suffix. The enum values
|
|
62
|
+
* themselves are untouched and stay greppable.
|
|
63
|
+
*/
|
|
64
|
+
const RECOMMENDED_SUFFIX = /\s*\((?:Recommended|Empfohlen|Default)\)\s*$/u;
|
|
65
|
+
|
|
46
66
|
/**
|
|
47
67
|
* Coordinator-facing AUQ question definition for the one-time capture.
|
|
48
68
|
* Mirrors the shape of `getInterviewQuestions()` entries in owner-interview.mjs:
|
|
49
69
|
* { question, header, options: [{ label, description }], multiSelect }
|
|
50
70
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
71
|
+
* Each label BEGINS with its ALLOWED_AUTONOMY value; option 1 additionally
|
|
72
|
+
* carries the `(Recommended)` marker AUQ-003 puts in the label. `coerceAutonomy()`
|
|
73
|
+
* strips that marker, so the selected label still maps to the enum value written
|
|
74
|
+
* by writeDispatcherAutonomyBlock() — see the comment on RECOMMENDED_SUFFIX for
|
|
75
|
+
* why the two halves must not be the same string.
|
|
53
76
|
* Option 1 (`off`) is the recommended, fail-closed default.
|
|
54
77
|
*
|
|
55
78
|
* @returns {{ question: string, header: string, options: Array<{ label: string, description: string }>, multiSelect: boolean }}
|
|
@@ -57,23 +80,23 @@ const DEFAULT_CONFIDENCE_FLOOR = 0.5;
|
|
|
57
80
|
export function getDispatcherAutonomyQuestion() {
|
|
58
81
|
return {
|
|
59
82
|
question:
|
|
60
|
-
'
|
|
61
|
-
header: 'Dispatcher
|
|
83
|
+
'When /dispatcher looks across your repos for the next one to work on, how may it treat this repo?',
|
|
84
|
+
header: 'Dispatcher',
|
|
62
85
|
options: [
|
|
63
86
|
{
|
|
64
|
-
label: 'off',
|
|
87
|
+
label: 'off (Recommended)',
|
|
65
88
|
description:
|
|
66
|
-
'
|
|
89
|
+
'Nothing changes: this repo is never offered and never started for you. Safe default — you keep picking the work.',
|
|
67
90
|
},
|
|
68
91
|
{
|
|
69
92
|
label: 'advisory',
|
|
70
93
|
description:
|
|
71
|
-
'
|
|
94
|
+
'This repo may appear in the ranked suggestions, so you can compare it against the others. You still start each session.',
|
|
72
95
|
},
|
|
73
96
|
{
|
|
74
97
|
label: 'autonomous-gated',
|
|
75
98
|
description:
|
|
76
|
-
'
|
|
99
|
+
'Work may start here without asking, but only when the ranking beats the confidence floor written next to this setting.',
|
|
77
100
|
},
|
|
78
101
|
],
|
|
79
102
|
multiSelect: false,
|
|
@@ -128,7 +151,7 @@ export function isDispatcherAutonomyBlockPresent(claudeMdContent) {
|
|
|
128
151
|
*/
|
|
129
152
|
function coerceAutonomy(value) {
|
|
130
153
|
if (value === null || value === undefined) return 'off';
|
|
131
|
-
const normalized = String(value).toLowerCase().trim();
|
|
154
|
+
const normalized = String(value).toLowerCase().replace(RECOMMENDED_SUFFIX, '').trim();
|
|
132
155
|
return ALLOWED_AUTONOMY.includes(normalized) ? normalized : 'off';
|
|
133
156
|
}
|
|
134
157
|
|
|
@@ -192,9 +192,20 @@ function _parseInlineObject(raw) {
|
|
|
192
192
|
*/
|
|
193
193
|
export function _parseResourceThresholds(kv) {
|
|
194
194
|
return {
|
|
195
|
+
// Memory thresholds are denominated in the signal `evaluate()` actually
|
|
196
|
+
// judges on — memory_pressure > ram_available > ram_free, in that order
|
|
197
|
+
// (#1089). They are NOT compared against Darwin's `os.freemem()` unless
|
|
198
|
+
// nothing better is published, which on Darwin never happens.
|
|
195
199
|
'ram-free-min-gb': _coerceInteger(kv, 'ram-free-min-gb', 4),
|
|
196
200
|
'ram-free-critical-gb': _coerceInteger(kv, 'ram-free-critical-gb', 2),
|
|
197
|
-
|
|
201
|
+
// 80 → 90 (#1089). At 80 this fired on 15.6% of 1477 measured session
|
|
202
|
+
// starts, largely on the decaying tail of the coordinator's own gate run;
|
|
203
|
+
// at 90 it fires on 12.8%, and under the two-signal rule it no longer caps
|
|
204
|
+
// anything on its own. Both numbers are measurements, not preferences.
|
|
205
|
+
'cpu-load-max-pct': _coerceInteger(kv, 'cpu-load-max-pct', 90),
|
|
206
|
+
// Unchanged at 5 — but now compared against LIVE PEER SESSIONS rather than
|
|
207
|
+
// the Claude process count. Same number, different denominator: measured
|
|
208
|
+
// firing rate drops from 93.6% to 4.2% (median processes:sessions = 6.0).
|
|
198
209
|
'concurrent-sessions-warn': _coerceInteger(kv, 'concurrent-sessions-warn', 5),
|
|
199
210
|
'ssh-no-docker': _coerceBoolean(kv, 'ssh-no-docker', true),
|
|
200
211
|
};
|
|
@@ -34,7 +34,7 @@ import path from 'node:path';
|
|
|
34
34
|
|
|
35
35
|
import { scanBacklog } from '../backlog-scan.mjs';
|
|
36
36
|
import { checkCiStatus as realCheckCiStatus } from '../ci-status-banner.mjs';
|
|
37
|
-
import { probe as realProbe, evaluate as realEvaluate } from '../resource-probe.mjs';
|
|
37
|
+
import { probe as realProbe, evaluate as realEvaluate, DEFAULT_RESOURCE_THRESHOLDS as CANONICAL_RESOURCE_THRESHOLDS } from '../resource-probe.mjs';
|
|
38
38
|
import { isRealSession } from '../session-schema/filters.mjs';
|
|
39
39
|
|
|
40
40
|
/** Staleness cap (days). Beyond this, additional age does not raise the score. */
|
|
@@ -49,12 +49,9 @@ const MS_PER_DAY = 86_400_000;
|
|
|
49
49
|
* Used by the default `resourceVerdict` dep so ranking works standalone
|
|
50
50
|
* without a parsed Session Config in hand.
|
|
51
51
|
*/
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
'cpu-load-max-pct': 80,
|
|
56
|
-
'concurrent-sessions-warn': 5,
|
|
57
|
-
};
|
|
52
|
+
// #1089: re-exported from the single canonical definition rather than a third
|
|
53
|
+
// hand-maintained copy — the three copies had already drifted apart.
|
|
54
|
+
const DEFAULT_RESOURCE_THRESHOLDS = { ...CANONICAL_RESOURCE_THRESHOLDS };
|
|
58
55
|
|
|
59
56
|
// ---------------------------------------------------------------------------
|
|
60
57
|
// PURE core
|
|
@@ -36,7 +36,7 @@ const startTime = Date.now();
|
|
|
36
36
|
const tcResult = runCheck(typecheckCmd);
|
|
37
37
|
const tcErrorCount =
|
|
38
38
|
tcResult.status === 'fail'
|
|
39
|
-
? extractCount(tcResult.output, /error TS\d+/)
|
|
39
|
+
? extractCount(tcResult.fullOutput ?? tcResult.output, /error TS\d+/)
|
|
40
40
|
: 0;
|
|
41
41
|
|
|
42
42
|
// --- Test ---
|
|
@@ -46,14 +46,14 @@ const tcErrorCount =
|
|
|
46
46
|
const testResult = runCheck(testCmd);
|
|
47
47
|
const { passed: testPassed, failed: testFailed, total: testTotal } =
|
|
48
48
|
testResult.status !== 'skip'
|
|
49
|
-
? extractTestCounts(testResult.output)
|
|
49
|
+
? extractTestCounts(testResult.fullOutput ?? testResult.output)
|
|
50
50
|
: { passed: 0, failed: 0, total: 0 };
|
|
51
51
|
|
|
52
52
|
// --- Lint ---
|
|
53
53
|
const lintResult = runCheck(lintCmd);
|
|
54
54
|
const lintWarnings =
|
|
55
55
|
lintResult.status !== 'skip'
|
|
56
|
-
? extractCount(lintResult.output, /warning/i)
|
|
56
|
+
? extractCount(lintResult.fullOutput ?? lintResult.output, /warning/i)
|
|
57
57
|
: 0;
|
|
58
58
|
|
|
59
59
|
// --- Debug artifacts ---
|