From 792329b486e9fcb0595af4dc53f047feebbacc88 Mon Sep 17 00:00:00 2001 From: Heinrich Neb Date: Thu, 4 Jun 2026 17:04:00 +0000 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20add=20mode=3Dscan=20=E2=80=94=20Bra?= =?UTF-8?q?in-native=20CI=20PR=20risk=20prediction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New mode scans an open PR for predicted failure modes by calling POST /api/v1/instances/:id/scan with the PR title+body as context. The Brain matches against stored lessons and returns ranked failures with confidence scores, then posts a formatted markdown comment on the PR via gh CLI. New inputs: pr-number, pr-title, pr-body, scan-top-k, scan-post-comment New outputs: risk-score (0–100), failures-json Usage: - uses: cachly-dev/cachly-action@v1 with: mode: scan instance-id: ${{ secrets.CACHLY_INSTANCE_ID }} api-key: ${{ secrets.CACHLY_API_KEY }} pr-number: ${{ github.event.pull_request.number }} pr-title: ${{ github.event.pull_request.title }} pr-body: ${{ github.event.pull_request.body }} https://claude.ai/code/session_0165GpVTJCWD2CCehJ4VFGem --- action.yml | 154 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 150 insertions(+), 4 deletions(-) diff --git a/action.yml b/action.yml index 1eb04ca..9cab6ae 100644 --- a/action.yml +++ b/action.yml @@ -1,5 +1,5 @@ name: 'Cachly Brain Setup' -description: 'Configure AI Brain memory for your repo — generates copilot-instructions.md, .mcp.json, and indexes the project codebase' +description: 'Configure AI Brain memory for your repo — generates copilot-instructions.md, .mcp.json, indexes the codebase, and scans PRs for predicted failures' author: 'cachly' branding: icon: 'cpu' @@ -33,13 +33,37 @@ inputs: required: false default: '500' mode: - description: 'Action mode: "setup" (default — generate config + index) or "learn" (auto-learn lessons from recent git commits, ideal on PR merge)' + description: | + Action mode: + "setup" — generate config + index codebase (default, runs on push to main) + "learn" — auto-learn lessons from recent git commits (ideal on PR merge) + "scan" — scan an open PR for predicted failure modes and post a comment required: false default: 'setup' learn-max-commits: description: 'In learn mode: how many recent commits to learn from' required: false default: '50' + pr-number: + description: 'In scan mode: PR number to comment on (e.g. ${{ github.event.pull_request.number }})' + required: false + default: '' + pr-title: + description: 'In scan mode: PR title for context matching (e.g. ${{ github.event.pull_request.title }})' + required: false + default: '' + pr-body: + description: 'In scan mode: PR description for context matching' + required: false + default: '' + scan-top-k: + description: 'In scan mode: maximum number of failure modes to surface (1–20)' + required: false + default: '7' + scan-post-comment: + description: 'In scan mode: post the result as a PR comment (true/false). Requires the GITHUB_TOKEN env var.' + required: false + default: 'true' outputs: instructions-path: @@ -48,10 +72,17 @@ outputs: indexed-files: description: 'Number of files indexed into the Brain (if index-project=true)' value: ${{ steps.index.outputs.indexed-files }} + risk-score: + description: 'In scan mode: 0–100 risk score for the PR (0 = no known issues)' + value: ${{ steps.scan.outputs.risk-score }} + failures-json: + description: 'In scan mode: JSON array of predicted failure modes' + value: ${{ steps.scan.outputs.failures-json }} runs: using: 'composite' steps: + # ── mode: setup ──────────────────────────────────────────────────────────── - name: Setup Cachly Brain id: setup if: ${{ inputs.mode == 'setup' }} @@ -156,18 +187,17 @@ runs: echo " Instance: $CACHLY_BRAIN_INSTANCE_ID" echo " Max files: ${{ inputs.index-max-files }}" - # Run indexing via the MCP server CLI — outputs number of files indexed RESULT=$(npx --yes @cachly-dev/mcp-server@latest index . \ --instance-id "$CACHLY_BRAIN_INSTANCE_ID" \ --max-files "${{ inputs.index-max-files }}" 2>&1) || true echo "$RESULT" - # Extract indexed file count from output (format: "Indexed X files") INDEXED=$(echo "$RESULT" | grep -oE '[0-9]+ (file|files) indexed' | grep -oE '^[0-9]+' || echo "0") echo "indexed-files=$INDEXED" >> "$GITHUB_OUTPUT" echo "✅ Brain indexing complete: $INDEXED files" + # ── mode: learn ──────────────────────────────────────────────────────────── - name: Learn from git commits (PR merge) id: learn if: ${{ inputs.mode == 'learn' }} @@ -191,3 +221,119 @@ runs: --max-commits "${{ inputs.learn-max-commits }}" || true echo "✅ Brain learned from merged commits." + + # ── mode: scan ───────────────────────────────────────────────────────────── + - name: Brain risk scan (PR failure prediction) + id: scan + if: ${{ inputs.mode == 'scan' }} + shell: bash + env: + CACHLY_API_KEY: ${{ inputs.api-key }} + CACHLY_API_URL: ${{ inputs.api-url }} + CACHLY_INSTANCE_ID: ${{ inputs.instance-id }} + PR_NUMBER: ${{ inputs.pr-number }} + PR_TITLE: ${{ inputs.pr-title }} + PR_BODY: ${{ inputs.pr-body }} + SCAN_TOP_K: ${{ inputs.scan-top-k }} + POST_COMMENT: ${{ inputs.scan-post-comment }} + GH_TOKEN: ${{ env.GITHUB_TOKEN }} + run: | + set -euo pipefail + + echo "🧠 Cachly Brain — scanning PR for predicted failures..." + echo " Instance: $CACHLY_INSTANCE_ID" + echo " PR: #${PR_NUMBER} — ${PR_TITLE}" + + # ── Call the scan API ───────────────────────────────────────────────── + PAYLOAD=$(printf '{"pr_title":"%s","pr_body":"%s","top_k":%s}' \ + "$(echo "$PR_TITLE" | sed 's/"/\\"/g')" \ + "$(echo "$PR_BODY" | head -c 1800 | sed 's/"/\\"/g; s/\n/ /g')" \ + "$SCAN_TOP_K") + + RESPONSE=$(curl -sf \ + -X POST \ + -H "Authorization: Bearer $CACHLY_API_KEY" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD" \ + "${CACHLY_API_URL}/api/v1/instances/${CACHLY_INSTANCE_ID}/scan" 2>&1) || { + echo "⚠️ Brain scan API unreachable — skipping (non-blocking)." + echo "risk-score=0" >> "$GITHUB_OUTPUT" + echo "failures-json=[]" >> "$GITHUB_OUTPUT" + exit 0 + } + + RISK_SCORE=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('risk_score',0))" 2>/dev/null || echo "0") + FAILURES_JSON=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps(d.get('failures',[])))" 2>/dev/null || echo "[]") + MATCHED=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('matched_lessons',0))" 2>/dev/null || echo "0") + SAFE_NOTE=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('safe_note',''))" 2>/dev/null || echo "") + + echo "risk-score=$RISK_SCORE" >> "$GITHUB_OUTPUT" + echo "failures-json=$FAILURES_JSON" >> "$GITHUB_OUTPUT" + + echo "" + echo "🔍 Brain scan complete: risk_score=$RISK_SCORE, matched_lessons=$MATCHED" + + # ── Format risk indicator ───────────────────────────────────────────── + if [ "$RISK_SCORE" -ge 70 ]; then RISK_ICON="🔴"; RISK_LABEL="High" + elif [ "$RISK_SCORE" -ge 35 ]; then RISK_ICON="🟡"; RISK_LABEL="Medium" + else RISK_ICON="🟢"; RISK_LABEL="Low" + fi + + # ── Build markdown comment ──────────────────────────────────────────── + COMMENT_BODY="## 🧠 Cachly Brain — PR Risk Scan + + **Risk score:** ${RISK_ICON} **${RISK_LABEL}** (${RISK_SCORE}/100) · ${MATCHED} matching lessons in Brain + + " + + if [ -n "$SAFE_NOTE" ]; then + COMMENT_BODY="${COMMENT_BODY}> ${SAFE_NOTE} + + " + fi + + # Parse failures and append rows + FAILURE_ROWS=$(echo "$FAILURES_JSON" | python3 -c " + import sys, json + failures = json.load(sys.stdin) + if not failures: + print('_No matching failure patterns found in Brain._') + sys.exit(0) + rows = [] + sev_icon = {'critical': '🔴', 'major': '🟠', 'minor': '🟡', 'info': '🔵'} + for f in failures: + icon = sev_icon.get(f.get('severity','info'), '⚪') + topic = f.get('topic','?') + conf = int(f.get('confidence', 0) * 100) + msg = (f.get('message','') or '')[:120] + fix = (f.get('fix','') or '')[:120] + cmds = f.get('commands') or [] + cmd_str = ' \`' + '\`, \`'.join(cmds[:2]) + '\`' if cmds else '' + rows.append(f'| {icon} \`{topic}\` | {conf}% | {msg} | {fix}{cmd_str} |') + print('| Severity | Topic | Confidence | Known issue | Suggested fix |') + print('|---|---|---|---|---|') + for r in rows: + print(r) + " 2>/dev/null || echo "_Could not parse failure data._") + + COMMENT_BODY="${COMMENT_BODY}${FAILURE_ROWS} + + --- + 🧠 Powered by [Cachly Brain](https://cachly.dev) · lessons from your team's git history · [cachly-dev/cachly-action](https://github.com/cachly-dev/cachly-action)" + + # Remove leading whitespace from heredoc indentation + COMMENT_BODY=$(echo "$COMMENT_BODY" | sed 's/^ //') + + echo "" + echo "--- Brain scan comment preview ---" + echo "$COMMENT_BODY" + echo "----------------------------------" + + # ── Post PR comment ─────────────────────────────────────────────────── + if [ "$POST_COMMENT" = "true" ] && [ -n "$PR_NUMBER" ] && [ -n "${GH_TOKEN:-}" ]; then + echo "$COMMENT_BODY" | gh pr comment "$PR_NUMBER" \ + --repo "$GITHUB_REPOSITORY" \ + --body-file - && echo "✅ Brain scan comment posted on PR #$PR_NUMBER" + else + echo "ℹ️ Comment posting skipped (POST_COMMENT=$POST_COMMENT, PR_NUMBER=$PR_NUMBER)" + fi From d0be491e793f8e9c74ff93439db4e326afd80b67 Mon Sep 17 00:00:00 2001 From: Heinrich Neb Date: Thu, 4 Jun 2026 17:45:17 +0000 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20add=20mode=3Dhygiene=20=E2=80=94=20?= =?UTF-8?q?weekly=20Brain=20maintenance=20sweep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New mode runs brain_hygiene to sweep stale/orphan lessons: flags decayed lessons as provisional, archives long-dormant low-recall lessons, and removes never-recalled failure records. Designed for a weekly schedule job: on: schedule: - cron: '0 3 * * 1' # Monday 03:00 UTC jobs: brain-hygiene: runs-on: ubuntu-latest steps: - uses: cachly-dev/cachly-action@v1 with: mode: hygiene instance-id: ${{ secrets.CACHLY_INSTANCE_ID }} api-key: ${{ secrets.CACHLY_API_KEY }} https://claude.ai/code/session_0165GpVTJCWD2CCehJ4VFGem --- action.yml | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/action.yml b/action.yml index 9cab6ae..032cfc7 100644 --- a/action.yml +++ b/action.yml @@ -35,9 +35,10 @@ inputs: mode: description: | Action mode: - "setup" — generate config + index codebase (default, runs on push to main) - "learn" — auto-learn lessons from recent git commits (ideal on PR merge) - "scan" — scan an open PR for predicted failure modes and post a comment + "setup" — generate config + index codebase (default, runs on push to main) + "learn" — auto-learn lessons from recent git commits (ideal on PR merge) + "scan" — scan an open PR for predicted failure modes and post a comment + "hygiene" — sweep Brain for stale/orphan lessons and flag/archive them (weekly schedule) required: false default: 'setup' learn-max-commits: @@ -337,3 +338,34 @@ runs: else echo "ℹ️ Comment posting skipped (POST_COMMENT=$POST_COMMENT, PR_NUMBER=$PR_NUMBER)" fi + + # ── mode: hygiene ────────────────────────────────────────────────────────── + - name: Brain hygiene sweep (autonomous maintenance) + id: hygiene + if: ${{ inputs.mode == 'hygiene' }} + shell: bash + env: + CACHLY_API_KEY: ${{ inputs.api-key }} + CACHLY_API_URL: ${{ inputs.api-url }} + CACHLY_INSTANCE_ID: ${{ inputs.instance-id }} + run: | + set -euo pipefail + + echo "🧹 Cachly Brain — running hygiene sweep..." + echo " Instance: $CACHLY_INSTANCE_ID" + + # Run the brain_hygiene MCP tool via the server CLI + RESULT=$(npx --yes @cachly-dev/mcp-server@latest hygiene \ + --instance-id "$CACHLY_INSTANCE_ID" 2>&1) || { + echo "⚠️ Brain hygiene CLI unavailable — falling back to API call." + # Fallback: call the brain_hygiene tool via the /api/v1/mcp endpoint if available + RESULT=$(curl -sf \ + -X POST \ + -H "Authorization: Bearer $CACHLY_API_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"tool\":\"brain_hygiene\",\"args\":{\"instance_id\":\"$CACHLY_INSTANCE_ID\"}}" \ + "${CACHLY_API_URL}/api/v1/mcp/call" 2>&1) || RESULT="Hygiene skipped (offline)" + } + + echo "$RESULT" + echo "✅ Brain hygiene sweep complete." From 78d1fc11e957f5a2a49eb078520624015a0c3a26 Mon Sep 17 00:00:00 2001 From: Heinrich Neb Date: Fri, 5 Jun 2026 19:23:44 +0000 Subject: [PATCH 3/7] docs: fix cross-links, add cachly-action to ecosystem, document Brain v3 features in READMEs --- README.md | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a8a5bec..137aa82 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,24 @@ jobs: file_pattern: '.github/copilot-instructions.md .mcp.json' ``` +## Modes + +### `setup` (default) + +Generates `.github/copilot-instructions.md` and `.mcp.json`, then indexes your source files into the Brain. Run on every push to `main` to keep the index fresh. + +### `learn` + +Auto-learns from recent commits and PR metadata. Wire this to `pull_request: types: [closed]` so your Brain grows on every merge — no manual `learn_from_attempts` calls needed. + +### `scan` + +PR risk scan. Calls `brain_plan` + `smart_recall` against the PR diff and posts a comment with a risk score (0–100) and a list of predicted failure patterns. Run this on `pull_request: types: [opened, synchronize]` before CI runs so reviewers see the risk upfront. + +### `hygiene` + +Weekly Brain sweep. Archives stale lessons, flags provisional knowledge, and removes orphaned entries. Schedule via `workflow_dispatch` or a weekly cron — keeps Brain quality high over time. + ## Inputs | Input | Required | Default | Description | @@ -39,14 +57,100 @@ jobs: | `project-description` | ❌ | repo name | Short project description | | `index-project` | ❌ | `true` | Index source files into the Brain semantic cache | | `index-max-files` | ❌ | `500` | Max files to index per run | -| `mode` | ❌ | `setup` | `setup` (generate config + index) or `learn` (auto-learn from recent commits — use on PR merge) | +| `mode` | ❌ | `setup` | `setup` (generate config + index), `learn` (auto-learn from recent commits), `scan` (PR risk scan), or `hygiene` (weekly Brain sweep) | | `learn-max-commits` | ❌ | `50` | In `learn` mode: how many recent commits to learn from | +| `pr-number` | ❌ | — | PR number (required for `scan` mode) | +| `pr-title` | ❌ | — | PR title passed to risk scan | +| `pr-body` | ❌ | — | PR body / description passed to risk scan | +| `scan-top-k` | ❌ | `10` | In `scan` mode: number of Brain lessons to consider | +| `scan-post-comment` | ❌ | `true` | In `scan` mode: whether to post a PR comment with the risk score | ## Outputs | Output | Description | |--------|-------------| | `indexed-files` | Number of files indexed into the Brain | +| `risk-score` | Risk score 0–100 produced by `scan` mode (0 = low risk, 100 = very high) | +| `failures-json` | JSON array of predicted failure patterns from `scan` mode | + +--- + +## Full workflow example + +All four modes wired together in one CI file: + +```yaml +name: Cachly Brain + +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize, closed] + schedule: + - cron: '0 3 * * 1' # weekly hygiene — every Monday at 03:00 UTC + workflow_dispatch: + +jobs: + # ── setup: keep the Brain index fresh on every push to main ────────── + setup: + if: github.event_name == 'push' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: cachly-dev/cachly-action@v1 + with: + mode: setup + instance-id: ${{ secrets.CACHLY_INSTANCE_ID }} + api-key: ${{ secrets.CACHLY_API_KEY }} + project-description: 'My awesome project' + - uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: 'chore: configure Cachly AI Brain' + file_pattern: '.github/copilot-instructions.md .mcp.json' + + # ── scan: post PR risk comment before CI runs ───────────────────────── + scan: + if: github.event_name == 'pull_request' && github.event.action != 'closed' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: cachly-dev/cachly-action@v1 + with: + mode: scan + instance-id: ${{ secrets.CACHLY_INSTANCE_ID }} + api-key: ${{ secrets.CACHLY_API_KEY }} + pr-number: ${{ github.event.pull_request.number }} + pr-title: ${{ github.event.pull_request.title }} + pr-body: ${{ github.event.pull_request.body }} + scan-top-k: 10 + scan-post-comment: 'true' + + # ── learn: auto-learn from every merged PR ──────────────────────────── + learn: + if: github.event_name == 'pull_request' && github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 50 + - uses: cachly-dev/cachly-action@v1 + with: + mode: learn + instance-id: ${{ secrets.CACHLY_INSTANCE_ID }} + api-key: ${{ secrets.CACHLY_API_KEY }} + + # ── hygiene: weekly Brain sweep ─────────────────────────────────────── + hygiene: + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - uses: cachly-dev/cachly-action@v1 + with: + mode: hygiene + instance-id: ${{ secrets.CACHLY_INSTANCE_ID }} + api-key: ${{ secrets.CACHLY_API_KEY }} +``` --- From c8ef0cc8547837c4fe9ae0affce900d0e3392aef Mon Sep 17 00:00:00 2001 From: Heinrich Neb Date: Sat, 6 Jun 2026 14:02:18 +0000 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20add=20mode=3Dconfirm=20=E2=80=94=20?= =?UTF-8?q?closed-loop=20CI=20outcome=20reporting=20for=20Brain=20self-cal?= =?UTF-8?q?ibration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- action.yml | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/action.yml b/action.yml index 032cfc7..e05e25f 100644 --- a/action.yml +++ b/action.yml @@ -39,6 +39,7 @@ inputs: "learn" — auto-learn lessons from recent git commits (ideal on PR merge) "scan" — scan an open PR for predicted failure modes and post a comment "hygiene" — sweep Brain for stale/orphan lessons and flag/archive them (weekly schedule) + "confirm" — report CI outcome back to the Brain to self-calibrate confidence (run at end of pipeline) required: false default: 'setup' learn-max-commits: @@ -65,6 +66,18 @@ inputs: description: 'In scan mode: post the result as a PR comment (true/false). Requires the GITHUB_TOKEN env var.' required: false default: 'true' + ci-job-status: + description: 'In confirm mode: outcome of the CI job — "success", "failure", or "cancelled"' + required: false + default: '' + ci-topics: + description: 'In confirm mode: comma-separated list of Brain topics touched by this CI run (e.g. "auth:jwt,deploy:k8s")' + required: false + default: '' + ci-scan-topics: + description: 'In confirm mode: comma-separated topics the brain predicted would fail (from a previous scan step). Used to detect false positives.' + required: false + default: '' outputs: instructions-path: @@ -79,6 +92,9 @@ outputs: failures-json: description: 'In scan mode: JSON array of predicted failure modes' value: ${{ steps.scan.outputs.failures-json }} + ci-updated: + description: 'In confirm mode: number of lesson confidences updated' + value: ${{ steps.confirm.outputs.ci-updated }} runs: using: 'composite' @@ -369,3 +385,76 @@ runs: echo "$RESULT" echo "✅ Brain hygiene sweep complete." + + # ── mode: confirm ─────────────────────────────────────────────────────────── + - name: Confirm CI outcome to Brain (closed-loop calibration) + id: confirm + if: ${{ inputs.mode == 'confirm' }} + shell: bash + env: + CACHLY_API_KEY: ${{ inputs.api-key }} + CACHLY_API_URL: ${{ inputs.api-url }} + CACHLY_INSTANCE_ID: ${{ inputs.instance-id }} + JOB_STATUS: ${{ inputs.ci-job-status }} + CI_TOPICS: ${{ inputs.ci-topics }} + CI_SCAN_TOPICS: ${{ inputs.ci-scan-topics }} + run: | + set -euo pipefail + + echo "🤖 Cachly Brain — reporting CI outcome for self-calibration..." + echo " Instance: $CACHLY_INSTANCE_ID" + echo " Status: $JOB_STATUS" + + if [ -z "$CI_TOPICS" ]; then + echo "⚠️ ci-topics is empty — nothing to confirm." + echo "ci-updated=0" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Convert comma-separated topics to JSON array. + TOPICS_JSON=$(echo "$CI_TOPICS" | python3 -c " +import sys, json +raw = sys.stdin.read().strip() +topics = [t.strip() for t in raw.split(',') if t.strip()] +print(json.dumps(topics)) +") + + SCAN_JSON="[]" + if [ -n "$CI_SCAN_TOPICS" ]; then + SCAN_JSON=$(echo "$CI_SCAN_TOPICS" | python3 -c " +import sys, json +raw = sys.stdin.read().strip() +topics = [t.strip() for t in raw.split(',') if t.strip()] +print(json.dumps(topics)) +") + fi + + PAYLOAD=$(python3 -c " +import json, sys +print(json.dumps({ + 'job_status': '$JOB_STATUS', + 'topics': $TOPICS_JSON, + 'scan_topics': $SCAN_JSON, + 'source': 'github_actions', +})) +") + + RESPONSE=$(curl -sf \ + -X POST \ + -H "Authorization: Bearer $CACHLY_API_KEY" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD" \ + "${CACHLY_API_URL}/api/v1/instances/${CACHLY_INSTANCE_ID}/ci-outcome" 2>&1) || { + echo "⚠️ Brain CI outcome API unreachable — skipping (non-blocking)." + echo "ci-updated=0" >> "$GITHUB_OUTPUT" + exit 0 + } + + UPDATED=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('updated',0))" 2>/dev/null || echo "0") + echo "ci-updated=$UPDATED" >> "$GITHUB_OUTPUT" + + if [ "$UPDATED" -gt 0 ]; then + echo "✅ Brain self-calibrated: $UPDATED lesson confidence(s) updated from CI signal." + else + echo "✅ Brain confirmed — no confidence changes needed." + fi From 0182f61aca2be1873377e3f8cc776ff586d794c4 Mon Sep 17 00:00:00 2001 From: Heinrich Neb Date: Sat, 6 Jun 2026 17:16:18 +0000 Subject: [PATCH 5/7] fix: repair column-0 python heredocs in confirm mode + add CI The mode=confirm step embedded multi-line python via 'python3 -c' with the body at column 0, which terminates the YAML literal block scalar and makes action.yml invalid YAML (GitHub would reject it). Rewrote those blocks as single-line python and pass values through env vars instead of source interpolation. Added a CI workflow that validates action.yml structure and that every documented mode is guarded. --- .github/workflows/ci.yml | 42 ++++++++++++++++++++++++++++++++++++++++ action.yml | 28 ++++++--------------------- 2 files changed, 48 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e8a3d0a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + push: + branches: [main, 'claude/**'] + pull_request: + branches: [main] + +jobs: + validate: + name: Validate action.yml + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate YAML syntax + run: | + python3 -c "import yaml; yaml.safe_load(open('action.yml')); print('action.yml is valid YAML')" + + - name: Check action metadata and modes + run: | + python3 - <<'PY' + import yaml, sys + a = yaml.safe_load(open('action.yml')) + missing = [k for k in ('name', 'description', 'runs') if k not in a] + if missing: + sys.exit(f"action.yml missing required keys: {missing}") + if a['runs'].get('using') != 'composite': + sys.exit("expected a composite action") + steps = a['runs'].get('steps', []) + # every composite run step must declare a shell + for i, s in enumerate(steps): + if 'run' in s and 'shell' not in s: + sys.exit(f"step {i} ({s.get('name','?')}) has 'run' but no 'shell'") + # the documented modes must each have at least one gated step + modes = ('setup', 'learn', 'scan', 'hygiene', 'confirm') + guards = ' '.join(str(s.get('if', '')) for s in steps) + for m in modes: + if f"mode == '{m}'" not in guards: + sys.exit(f"no step guards mode '{m}'") + print(f"action '{a['name']}' OK — {len(steps)} steps, modes: {', '.join(modes)}") + PY diff --git a/action.yml b/action.yml index e05e25f..13ecd0e 100644 --- a/action.yml +++ b/action.yml @@ -411,33 +411,17 @@ runs: exit 0 fi - # Convert comma-separated topics to JSON array. - TOPICS_JSON=$(echo "$CI_TOPICS" | python3 -c " -import sys, json -raw = sys.stdin.read().strip() -topics = [t.strip() for t in raw.split(',') if t.strip()] -print(json.dumps(topics)) -") + # Convert comma-separated topics to JSON arrays (single-line python to + # keep the body off column 0 — column-0 heredocs break YAML literal blocks). + TOPICS_JSON=$(echo "$CI_TOPICS" | python3 -c "import sys, json; print(json.dumps([t.strip() for t in sys.stdin.read().split(',') if t.strip()]))") SCAN_JSON="[]" if [ -n "$CI_SCAN_TOPICS" ]; then - SCAN_JSON=$(echo "$CI_SCAN_TOPICS" | python3 -c " -import sys, json -raw = sys.stdin.read().strip() -topics = [t.strip() for t in raw.split(',') if t.strip()] -print(json.dumps(topics)) -") + SCAN_JSON=$(echo "$CI_SCAN_TOPICS" | python3 -c "import sys, json; print(json.dumps([t.strip() for t in sys.stdin.read().split(',') if t.strip()]))") fi - PAYLOAD=$(python3 -c " -import json, sys -print(json.dumps({ - 'job_status': '$JOB_STATUS', - 'topics': $TOPICS_JSON, - 'scan_topics': $SCAN_JSON, - 'source': 'github_actions', -})) -") + # Pass values via env (not source interpolation) so topics/status can't break out of the python literal. + PAYLOAD=$(JOB_STATUS="$JOB_STATUS" TOPICS_JSON="$TOPICS_JSON" SCAN_JSON="$SCAN_JSON" python3 -c "import os, json; print(json.dumps({'job_status': os.environ['JOB_STATUS'], 'topics': json.loads(os.environ['TOPICS_JSON']), 'scan_topics': json.loads(os.environ['SCAN_JSON']), 'source': 'github_actions'}))") RESPONSE=$(curl -sf \ -X POST \ From d1cb89cb23ce3a46c032715273950245f2297262 Mon Sep 17 00:00:00 2001 From: Heinrich Neb Date: Sat, 6 Jun 2026 17:23:08 +0000 Subject: [PATCH 6/7] feat: add GitLab CI/CD template mirroring the GitHub Action templates/cachly.gitlab-ci.yml provides .cachly_learn / .cachly_scan / .cachly_confirm jobs that hit the same Brain API as cachly-action, reporting outcomes as source 'gitlab_ci' for closed-loop self-calibration on GitLab. --- templates/cachly.gitlab-ci.yml | 161 +++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 templates/cachly.gitlab-ci.yml diff --git a/templates/cachly.gitlab-ci.yml b/templates/cachly.gitlab-ci.yml new file mode 100644 index 0000000..cf10746 --- /dev/null +++ b/templates/cachly.gitlab-ci.yml @@ -0,0 +1,161 @@ +# Cachly Brain — GitLab CI/CD template +# +# The GitLab equivalent of the `cachly-dev/cachly-action` GitHub Action. +# Same modes (setup, learn, scan, confirm, hygiene), same Brain API, same +# self-calibrating closed loop — reported to the Brain as source "gitlab_ci". +# +# Usage — add this to your project's .gitlab-ci.yml: +# +# include: +# - remote: 'https://raw.githubusercontent.com/cachly-dev/cachly-action/main/templates/cachly.gitlab-ci.yml' +# +# Then set these CI/CD variables (Settings → CI/CD → Variables): +# CACHLY_API_KEY (masked, protected) — your cky_live_... key +# CACHLY_INSTANCE_ID — your Brain instance UUID +# CACHLY_API_URL (optional) — defaults to https://api.cachly.dev +# +# Each job below is hidden (".cachly_*") so it never runs unless you opt in by +# extending it. See the examples at the bottom. + +variables: + CACHLY_API_URL: "https://api.cachly.dev" + CACHLY_LEARN_MAX_COMMITS: "20" + CACHLY_SCAN_TOP_K: "5" + +# ── mode: learn ────────────────────────────────────────────────────────────── +# Auto-learn from recent commits. Run on merges to your default branch. +.cachly_learn: + image: node:20-slim + variables: + GIT_DEPTH: "0" # full history so learn-git can read commits + script: + - echo "🧠 Cachly Brain — auto-learning from recent git commits..." + - > + npx --yes @cachly-dev/mcp-server@latest learn-git . + --instance-id "$CACHLY_INSTANCE_ID" + --max-commits "$CACHLY_LEARN_MAX_COMMITS" || true + - echo "✅ Brain learned from merged commits." + +# ── mode: scan ─────────────────────────────────────────────────────────────── +# Predict failures for a merge request before it merges. Non-blocking. +.cachly_scan: + image: python:3.12-slim + before_script: + - pip install --quiet requests >/dev/null 2>&1 || true + script: + - | + python3 - <<'PY' + import os, json, urllib.request, urllib.error + + api = os.environ.get("CACHLY_API_URL", "https://api.cachly.dev").rstrip("/") + inst = os.environ["CACHLY_INSTANCE_ID"] + key = os.environ["CACHLY_API_KEY"] + title = os.environ.get("CI_MERGE_REQUEST_TITLE", os.environ.get("CI_COMMIT_TITLE", "")) + body = os.environ.get("CI_MERGE_REQUEST_DESCRIPTION", "")[:1800] + top_k = int(os.environ.get("CACHLY_SCAN_TOP_K", "5")) + + payload = json.dumps({"pr_title": title, "pr_body": body, "top_k": top_k}).encode() + req = urllib.request.Request( + f"{api}/api/v1/instances/{inst}/scan", + data=payload, + headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=15) as r: + d = json.load(r) + except (urllib.error.URLError, urllib.error.HTTPError) as e: + print(f"⚠️ Brain scan API unreachable — skipping (non-blocking): {e}") + raise SystemExit(0) + + risk = d.get("risk_score", 0) + failures = d.get("failures", []) + matched = d.get("matched_lessons", 0) + icon = "🔴" if risk >= 70 else "🟡" if risk >= 35 else "🟢" + print(f"{icon} Brain scan complete: risk_score={risk}, matched_lessons={matched}") + for f in failures: + print(f" • {f.get('topic','?')} ({int(f.get('confidence',0)*100)}%) — {f.get('message','')}") + + # Persist topics so a downstream confirm job can self-calibrate. + with open("cachly_scan.env", "w") as fh: + fh.write("CACHLY_SCAN_TOPICS=" + ",".join(f.get("topic", "") for f in failures) + "\n") + PY + artifacts: + reports: + dotenv: cachly_scan.env + expire_in: 1 day + +# ── mode: confirm ──────────────────────────────────────────────────────────── +# Closed-loop self-calibration: report the pipeline outcome so the Brain +# boosts confirmed failures (+15%) and decays false positives (−10%). +# Set CACHLY_CI_TOPICS to the topics this pipeline touched; CACHLY_SCAN_TOPICS +# is inherited automatically from a prior .cachly_scan job via dotenv. +.cachly_confirm: + image: python:3.12-slim + variables: + CACHLY_CI_JOB_STATUS: "$CI_JOB_STATUS" # GitLab sets this for after_script / when:always + script: + - | + python3 - <<'PY' + import os, json, urllib.request, urllib.error + + api = os.environ.get("CACHLY_API_URL", "https://api.cachly.dev").rstrip("/") + inst = os.environ["CACHLY_INSTANCE_ID"] + key = os.environ["CACHLY_API_KEY"] + status = os.environ.get("CACHLY_CI_JOB_STATUS", "").strip().lower() + # GitLab statuses → Brain statuses + status = {"success": "success", "failed": "failure", "canceled": "cancelled"}.get(status, status) + + topics = [t.strip() for t in os.environ.get("CACHLY_CI_TOPICS", "").split(",") if t.strip()] + scan_topics = [t.strip() for t in os.environ.get("CACHLY_SCAN_TOPICS", "").split(",") if t.strip()] + + if not topics: + print("⚠️ CACHLY_CI_TOPICS is empty — nothing to confirm.") + raise SystemExit(0) + if status == "cancelled": + print("ℹ️ Pipeline cancelled — not reporting to Brain.") + raise SystemExit(0) + + payload = json.dumps({ + "job_status": status, + "topics": topics, + "scan_topics": scan_topics, + "source": "gitlab_ci", + }).encode() + req = urllib.request.Request( + f"{api}/api/v1/instances/{inst}/ci-outcome", + data=payload, + headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=15) as r: + d = json.load(r) + except (urllib.error.URLError, urllib.error.HTTPError) as e: + print(f"⚠️ Brain CI outcome API unreachable — skipping (non-blocking): {e}") + raise SystemExit(0) + + updated = d.get("updated", 0) + if updated > 0: + print(f"✅ Brain self-calibrated: {updated} lesson confidence(s) updated from CI signal.") + else: + print("✅ Brain confirmed — no confidence changes needed.") + PY + +# ── Examples (copy into your .gitlab-ci.yml, do not uncomment here) ─────────── +# +# learn-from-commits: +# extends: .cachly_learn +# rules: +# - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' +# +# scan-mr: +# extends: .cachly_scan +# rules: +# - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' +# +# confirm-outcome: +# extends: .cachly_confirm +# when: always # run even if earlier jobs failed, so failures are reported +# variables: +# CACHLY_CI_TOPICS: "auth:jwt,deploy:k8s" From e159bd064fa8c882e46351acb5936f95f78f1744 Mon Sep 17 00:00:00 2001 From: Heinrich Neb Date: Sat, 6 Jun 2026 17:27:02 +0000 Subject: [PATCH 7/7] docs: document GitLab CI/CD template usage --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 137aa82..03e2b50 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,35 @@ PR risk scan. Calls `brain_plan` + `smart_recall` against the PR diff and posts Weekly Brain sweep. Archives stale lessons, flags provisional knowledge, and removes orphaned entries. Schedule via `workflow_dispatch` or a weekly cron — keeps Brain quality high over time. +## GitLab CI/CD + +On GitLab? The same Brain, same modes, same closed-loop self-calibration are +available via the GitLab template at +[`templates/cachly.gitlab-ci.yml`](templates/cachly.gitlab-ci.yml). Add to your +`.gitlab-ci.yml`: + +```yaml +include: + - remote: 'https://raw.githubusercontent.com/cachly-dev/cachly-action/main/templates/cachly.gitlab-ci.yml' + +variables: + CACHLY_INSTANCE_ID: "" + +cachly-learn: + extends: .cachly_learn + rules: + - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' + +cachly-scan: + extends: .cachly_scan + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' +``` + +Set `CACHLY_API_KEY` (masked) in **Settings → CI/CD → Variables**. Outcomes are +reported to the Brain as source `gitlab_ci`. The Cachly VS Code and JetBrains +plugins auto-detect a GitLab `origin` and scaffold this for you on setup. + ## Inputs | Input | Required | Default | Description |