Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepgatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
claude-config.test.ts5.7 KB · 163 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/**
 * Block W2 — Claude harness configuration.
 *
 * Coverage:
 *   - .claude/settings.json is valid JSON
 *   - It declares the `gluecron` MCP server with the expected URL pattern
 *   - It denies the canonical 11 GitHub-write tool names
 *   - CLAUDE.md contains the "Source of truth: Gluecron" section
 *   - Each .claude/skills/<name>/SKILL.md exists with the expected
 *     frontmatter shape (name, description, tools)
 */

import { describe, it, expect } from "bun:test";
import { readFile } from "node:fs/promises";
import { join } from "node:path";

const REPO_ROOT = join(import.meta.dir, "..", "..");
const SETTINGS_PATH = join(REPO_ROOT, ".claude", "settings.json");
const CLAUDE_MD = join(REPO_ROOT, "CLAUDE.md");
const SKILLS_DIR = join(REPO_ROOT, ".claude", "skills");

const SKILLS = ["gluecron-pr", "gluecron-issue", "gluecron-review"] as const;

const DENIED_GITHUB_WRITE_TOOLS = [
  "mcp__github__create_pull_request",
  "mcp__github__merge_pull_request",
  "mcp__github__create_or_update_file",
  "mcp__github__push_files",
  "mcp__github__delete_file",
  "mcp__github__create_branch",
  "mcp__github__create_repository",
  "mcp__github__create_pull_request_with_copilot",
  "mcp__github__update_pull_request",
  "mcp__github__update_pull_request_branch",
  "mcp__github__merge_pull_request_with_copilot",
];

// ---------------------------------------------------------------------------
// .claude/settings.json shape
// ---------------------------------------------------------------------------

describe(".claude/settings.json", () => {
  it("is valid JSON", async () => {
    const raw = await readFile(SETTINGS_PATH, "utf8");
    expect(() => JSON.parse(raw)).not.toThrow();
  });

  it("declares the gluecron MCP server", async () => {
    const raw = await readFile(SETTINGS_PATH, "utf8");
    const cfg = JSON.parse(raw) as {
      mcpServers?: Record<string, { transport?: string; url?: string }>;
    };
    expect(cfg.mcpServers).toBeDefined();
    expect(cfg.mcpServers!.gluecron).toBeDefined();
    const entry = cfg.mcpServers!.gluecron!;
    expect(entry.transport).toBe("http");
    // URL ends with /mcp on a gluecron host.
    expect(entry.url).toMatch(/gluecron[^\s]*\/mcp$/);
  });

  it("passes Authorization via env var (PAT never in file)", async () => {
    const raw = await readFile(SETTINGS_PATH, "utf8");
    const cfg = JSON.parse(raw) as {
      mcpServers?: Record<
        string,
        { headers?: Record<string, string> }
      >;
    };
    const auth = cfg.mcpServers?.gluecron?.headers?.Authorization;
    expect(auth).toBeDefined();
    expect(auth).toContain("${env:GLUECRON_PAT}");
    expect(auth).toMatch(/^Bearer\s/);
  });

  it(`denies all ${DENIED_GITHUB_WRITE_TOOLS.length} canonical GitHub write tools`, async () => {
    const raw = await readFile(SETTINGS_PATH, "utf8");
    const cfg = JSON.parse(raw) as {
      permissions?: { deny?: string[] };
    };
    expect(cfg.permissions?.deny).toBeDefined();
    const deny = cfg.permissions!.deny!;
    for (const tool of DENIED_GITHUB_WRITE_TOOLS) {
      expect(deny).toContain(tool);
    }
  });
});

// ---------------------------------------------------------------------------
// CLAUDE.md "Source of truth: Gluecron" section
// ---------------------------------------------------------------------------

describe("CLAUDE.md", () => {
  it("contains the 'Source of truth: Gluecron' section", async () => {
    const raw = await readFile(CLAUDE_MD, "utf8");
    expect(raw).toContain("## Source of truth: Gluecron (self-canonical, no GitHub mirror)");
  });

  it("references .claude/settings.json and the mcp-tools module", async () => {
    const raw = await readFile(CLAUDE_MD, "utf8");
    expect(raw).toContain(".claude/settings.json");
    expect(raw).toContain("src/lib/mcp-tools.ts");
  });

  it("warns against calling mcp__github__* write tools", async () => {
    const raw = await readFile(CLAUDE_MD, "utf8");
    expect(raw).toContain("mcp__github__");
  });

  it("mentions the GLUECRON_PAT env var", async () => {
    const raw = await readFile(CLAUDE_MD, "utf8");
    expect(raw).toContain("GLUECRON_PAT");
  });
});

// ---------------------------------------------------------------------------
// Each .claude/skills/<name>/SKILL.md exists and has the expected frontmatter
// ---------------------------------------------------------------------------

function parseFrontmatter(src: string): Record<string, string> {
  if (!src.startsWith("---")) {
    throw new Error("SKILL.md missing frontmatter");
  }
  const end = src.indexOf("\n---", 3);
  if (end < 0) throw new Error("SKILL.md frontmatter not terminated");
  const raw = src.slice(3, end).trim();
  const fields: Record<string, string> = {};
  let currentKey = "";
  let buffer: string[] = [];
  const flush = () => {
    if (currentKey) {
      fields[currentKey] = buffer.join("\n").trim();
    }
    buffer = [];
  };
  for (const line of raw.split("\n")) {
    const m = /^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/.exec(line);
    if (m && !line.startsWith(" ") && !line.startsWith("\t")) {
      flush();
      currentKey = m[1];
      buffer = [m[2]];
    } else {
      buffer.push(line);
    }
  }
  flush();
  return fields;
}

describe(".claude/skills/* SKILL.md shape", () => {
  for (const name of SKILLS) {
    it(`${name}/SKILL.md exists and has name+description+tools`, async () => {
      const path = join(SKILLS_DIR, name, "SKILL.md");
      const raw = await readFile(path, "utf8");
      const fields = parseFrontmatter(raw);
      expect(fields.name).toBe(name);
      expect(fields.description).toBeDefined();
      expect(fields.description.length).toBeGreaterThan(0);
      expect(fields.description.toLowerCase()).toContain("gluecron");
      expect(fields.tools).toBeDefined();
      expect(fields.tools.length).toBeGreaterThan(0);
    });
  }
});