@compr/opscontext-mcp 2.8.0 → 2.8.1

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/CHANGELOG.md CHANGED
@@ -4,6 +4,34 @@ All notable changes to OpsContext for AI Agents (previously ContextEngine — MC
4
4
 
5
5
  > Entries for 2.2.0 through 2.4.0 were not backfilled here; see `docs/sessions/SESSION_19` through `SESSION_21` for those releases.
6
6
 
7
+ ## [2.8.1] 2026-09-07: a push is not done until its CI is read
8
+
9
+ ### Added
10
+
11
+ - **`end-session` check 3c, CI on HEAD** (`[PUSHED-MEANS-CI-READ]`, `src/ci-status.ts`): every
12
+ workflow run for the exact HEAD sha through `gh run list`; a failed run is a FAIL item (exit 1).
13
+ No gh, no remote, or no runs yet is "not checked", never a pass. Why: main's CI had been red
14
+ on every commit since 2026-09-04 and the Telegram alert fired each time; thirty commits and
15
+ five releases went by with nobody reading it.
16
+
17
+ ### Changed
18
+
19
+ - CI installs the activation server's own packages too: the first time the Test step actually
20
+ ran, `server/src/community-rules-server.test.ts` could not load (`ERR_MODULE_NOT_FOUND`).
21
+ - The Doc Freshness gate fails only when 20 or more source lines change without a doc change; a
22
+ one-line lint fix is not a documented change (its first run paged Telegram for a dash).
23
+ - CI runs on Node 20 and 22 with `fail-fast: false`; Node 18 is EOL and eslint 10 needs 20.19+,
24
+ and the 18 job's failure was cancelling the others before tests ran. `engines.node` is now
25
+ `>=20.19.0`, which is what is tested.
26
+
27
+ ### Fixed
28
+
29
+ - CI was red on every pull request for one `prefer-const` lint error in `src/firewall.ts`.
30
+ - The Doc Freshness workflow failed any push or PR made more than 8 hours after the last
31
+ SKILLS.md commit, whatever the change (two Dependabot bumps on 2026-09-07). It now measures
32
+ the change itself: source touched without a doc touched fails, anything else passes
33
+ (`[DOC-GATE-MEASURES-THE-DIFF]`).
34
+
7
35
  ## [2.8.0] 2026-09-06: health is measured, never estimated
8
36
 
9
37
  ### Added
@@ -0,0 +1,16 @@
1
+ export interface CiRun {
2
+ name: string;
3
+ status: string;
4
+ conclusion: string | null;
5
+ url: string;
6
+ }
7
+ export interface CiStatus {
8
+ sha: string;
9
+ state: "ok" | "failed" | "pending" | "no-runs" | "unavailable";
10
+ runs: CiRun[];
11
+ note?: string;
12
+ }
13
+ export type Runner = (cmd: string, args: string[], cwd: string) => string;
14
+ export declare function ciStatusForHead(cwd: string, run?: Runner): CiStatus;
15
+ export declare function formatCiStatus(s: CiStatus): string[];
16
+ //# sourceMappingURL=ci-status.d.ts.map
@@ -0,0 +1,55 @@
1
+ // [LOCKED] [PUSHED-MEANS-CI-READ] 2026-09-07
2
+ // [NEVER] let end-session pass while a workflow run for HEAD has failed, and [NEVER] count
3
+ // "no runs found" as green.
4
+ // WHY: main's CI had been red on every commit since 2026-09-04 (one lint error, then a Node 18
5
+ // job that eslint 10 cannot run on) and the Telegram alert fired each time. Thirty commits,
6
+ // five releases, nobody read it: the post-commit hook pushes, end-session ran after every
7
+ // push, and nothing in that loop looked at the result. A push is not done until its CI is.
8
+ // FIX: end-session check 3c lists every workflow run for the exact HEAD sha through `gh run
9
+ // list` (hardcoded argv, no shell) and counts a failure as a FAIL item. gh missing, no
10
+ // GitHub remote, or no runs yet is reported as "not checked", never as pass.
11
+ import { execFileSync } from "child_process";
12
+ const defaultRunner = (cmd, args, cwd) => execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 15_000 }).trim();
13
+ export function ciStatusForHead(cwd, run = defaultRunner) {
14
+ let sha = "";
15
+ try {
16
+ sha = run("git", ["rev-parse", "HEAD"], cwd);
17
+ }
18
+ catch {
19
+ return { sha, state: "unavailable", runs: [], note: "not a git repository" };
20
+ }
21
+ let raw = "";
22
+ try {
23
+ raw = run("gh", ["run", "list", "--limit", "40", "--json", "name,status,conclusion,url,headSha"], cwd);
24
+ }
25
+ catch {
26
+ return { sha, state: "unavailable", runs: [], note: "gh not available, not logged in, or no GitHub remote" };
27
+ }
28
+ let all = [];
29
+ try {
30
+ all = JSON.parse(raw);
31
+ }
32
+ catch {
33
+ return { sha, state: "unavailable", runs: [], note: "gh returned no JSON" };
34
+ }
35
+ const runs = all.filter((r) => r.headSha === sha).map(({ name, status, conclusion, url }) => ({ name, status, conclusion, url }));
36
+ if (runs.length === 0)
37
+ return { sha, state: "no-runs", runs, note: "no workflow run for HEAD yet: pushed seconds ago, or CI not wired" };
38
+ const failed = runs.some((r) => r.conclusion === "failure" || r.conclusion === "timed_out" || r.conclusion === "startup_failure");
39
+ const pending = runs.some((r) => r.status !== "completed");
40
+ return { sha, state: failed ? "failed" : pending ? "pending" : "ok", runs };
41
+ }
42
+ export function formatCiStatus(s) {
43
+ const lines = [];
44
+ if (s.state === "unavailable" || s.state === "no-runs") {
45
+ lines.push(`- ⚠️ CI on HEAD${s.sha ? ` ${s.sha.slice(0, 7)}` : ""} not checked: ${s.note}`);
46
+ return lines;
47
+ }
48
+ for (const r of s.runs) {
49
+ const bad = r.conclusion === "failure" || r.conclusion === "timed_out" || r.conclusion === "startup_failure";
50
+ const icon = bad ? "❌ FAIL" : r.status !== "completed" ? "⏳" : r.conclusion === "success" ? "✅" : "▫️";
51
+ lines.push(`- ${icon} ${r.name}: ${r.conclusion ?? r.status}${bad ? ` ${r.url}` : ""}`);
52
+ }
53
+ return lines;
54
+ }
55
+ //# sourceMappingURL=ci-status.js.map
package/dist/cli.js CHANGED
@@ -657,6 +657,7 @@ import { getStagedFiles, runSecretScan, runDocCoverage, runCommitMessageRequired
657
657
  import { safeAppend } from "./audit.js";
658
658
  import { listServers, formatServers } from "./server-registry.js";
659
659
  import { computeFleetHealth, formatFleetHealth } from "./fleet-health.js";
660
+ import { ciStatusForHead, formatCiStatus } from "./ci-status.js";
660
661
  import { installSkill, locateBundledSkill, buildManagedBlock, syncClaudeMd, } from "./claude-integration.js";
661
662
  import { fileURLToPath } from "url";
662
663
  // ---------------------------------------------------------------------------
@@ -2238,6 +2239,17 @@ async function cliEndSession() {
2238
2239
  if (fleet.warnings.length > 0)
2239
2240
  failCount += fleet.warnings.length;
2240
2241
  checks.push("");
2242
+ // --- Check 3c: CI on HEAD ([LOCK] [PUSHED-MEANS-CI-READ]) ---
2243
+ checks.push("## 3c. CI on HEAD\n");
2244
+ const ci = ciStatusForHead(process.cwd());
2245
+ checks.push(...formatCiStatus(ci));
2246
+ if (ci.state === "failed") {
2247
+ failCount++;
2248
+ checks.push("- ❌ FAIL: a workflow run for HEAD failed; a push is not done until its CI is");
2249
+ }
2250
+ else if (ci.state === "ok")
2251
+ passCount++;
2252
+ checks.push("");
2241
2253
  checks.push("## 4. Sessions\n");
2242
2254
  const sessions = listSessions();
2243
2255
  if (sessions.length > 0) {
package/dist/firewall.js CHANGED
@@ -185,7 +185,7 @@ export class ProtocolFirewall {
185
185
  if (sessionUrgent && level === "footer")
186
186
  level = "header";
187
187
  // Prepend learning injection to response (always, if available)
188
- let text = injection ? injection + "\n\n" + responseText : responseText;
188
+ const text = injection ? injection + "\n\n" + responseText : responseText;
189
189
  // Build session urgency block (always prepended when overdue)
190
190
  const urgentBlock = sessionUrgent ? this.buildSessionUrgentBlock() : null;
191
191
  if (level === "silent" && !urgentBlock)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compr/opscontext-mcp",
3
- "version": "2.8.0",
3
+ "version": "2.8.1",
4
4
  "description": "OpsContext for AI Agents — read-only fleet visibility (PM2/nginx/Docker/git/cron) + tamper-evident audit log + policy-as-code hooks. The ops + compliance layer Claude Code can't grow natively.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -59,7 +59,7 @@
59
59
  "email": "yannick@compr.ch"
60
60
  },
61
61
  "engines": {
62
- "node": ">=18.0.0"
62
+ "node": ">=20.19.0"
63
63
  },
64
64
  "files": [
65
65
  "dist/",