@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 +28 -0
- package/dist/ci-status.d.ts +16 -0
- package/dist/ci-status.js +55 -0
- package/dist/cli.js +12 -0
- package/dist/firewall.js +1 -1
- package/package.json +2 -2
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
|
-
|
|
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.
|
|
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": ">=
|
|
62
|
+
"node": ">=20.19.0"
|
|
63
63
|
},
|
|
64
64
|
"files": [
|
|
65
65
|
"dist/",
|