@cxi-lmai/ci-agent-platform 3.0.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/LICENSE +21 -0
- package/README.md +219 -0
- package/bin/init.mjs +236 -0
- package/package.json +47 -0
- package/payload/INSTALL.md +113 -0
- package/payload/agents/agent-architect.md +101 -0
- package/payload/agents/code-reviewer.md +87 -0
- package/payload/agents/codebase-auditor.md +73 -0
- package/payload/agents/coder.md +56 -0
- package/payload/agents/decomposer.md +70 -0
- package/payload/agents/docs-sync.md +115 -0
- package/payload/agents/e2e-test-writer.md +47 -0
- package/payload/agents/migration-reviewer.md +100 -0
- package/payload/agents/orchestrator.md +50 -0
- package/payload/agents/performance-reviewer.md +82 -0
- package/payload/agents/postmortem.md +83 -0
- package/payload/agents/release-mr.md +274 -0
- package/payload/agents/security-reviewer.md +122 -0
- package/payload/agents/test-fix.md +33 -0
- package/payload/agents/test-writer.md +40 -0
- package/payload/ci-templates/claude-pipeline.gitlab-ci.yml +233 -0
- package/payload/ci-templates/github/README.md +76 -0
- package/payload/ci-templates/github/claude-issue-pipeline.yml +141 -0
- package/payload/ci-templates/github/claude-pipeline.yml +141 -0
- package/payload/ci-templates/github/claude-test-fix.yml +104 -0
- package/payload/ci-templates/scripts/code.sh +114 -0
- package/payload/ci-templates/scripts/lib/issue-loop.sh +430 -0
- package/payload/ci-templates/scripts/lib/pipeline-common.sh +280 -0
- package/payload/ci-templates/scripts/lib/platform.sh +177 -0
- package/payload/ci-templates/scripts/lib/usage-capture.sh +110 -0
- package/payload/ci-templates/scripts/orchestrate.sh +294 -0
- package/payload/ci-templates/scripts/postmortem.sh +45 -0
- package/payload/ci-templates/scripts/review-fix.sh +90 -0
- package/payload/ci-templates/scripts/review.sh +93 -0
- package/payload/ci-templates/scripts/test-fix.sh +58 -0
- package/payload/skills/fix-review-findings/SKILL.md +79 -0
- package/payload/skills/fix-tests/SKILL.md +70 -0
- package/payload/skills/implement-issue/SKILL.md +62 -0
- package/payload/skills/init-pipeline-config/SKILL.md +96 -0
- package/payload/skills/postmortem-mr/SKILL.md +50 -0
- package/payload/skills/review-mr/SKILL.md +82 -0
- package/payload/skills/triage-issue/SKILL.md +74 -0
- package/payload/templates/pipeline-config.template.md +98 -0
- package/payload/templates/review_suppressions.template.md +25 -0
- package/payload/templates/spec-issue.template.md +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Technical University of Liberec
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# ci-agent-platform
|
|
2
|
+
|
|
3
|
+
> Autonomous dev pipeline on top of plain GitLab CI / GitHub Actions, driven by Claude Code.
|
|
4
|
+
|
|
5
|
+
The core is the **issue-to-code** loop: you file an issue and an open MR/PR comes
|
|
6
|
+
out. The orchestrator triages it, the coder implements it, and the review loop
|
|
7
|
+
checks the result. The input is a spec, the output is code. Details in
|
|
8
|
+
[docs/issue-to-code.md](https://gitlab.com/cxi-lmai/ci-agent-platform/-/blob/main/docs/issue-to-code.md).
|
|
9
|
+
|
|
10
|
+
- **15 generic agents**: triage, coding, review, tests, docs, release.
|
|
11
|
+
- **7 skills**, configured per project.
|
|
12
|
+
- **Project specifics live outside the agents**, in `.claude/pipeline-config.md` and `PIPE_*` variables.
|
|
13
|
+
|
|
14
|
+
> [!NOTE]
|
|
15
|
+
> The CI template wires two loops:
|
|
16
|
+
> - the **review loop** (`review`, `review-fix`, `test-fix`, and the shared escalation `/postmortem-mr`),
|
|
17
|
+
> - the **issue-to-code loop** (`orchestrate`, `code`, skills `triage-issue` and `implement-issue`).
|
|
18
|
+
>
|
|
19
|
+
> The other agents are installed too but have no CI job of their own. You invoke them by hand from the command line.
|
|
20
|
+
|
|
21
|
+
> [!CAUTION]
|
|
22
|
+
> The coder runs with `--dangerously-skip-permissions` and treats issue content
|
|
23
|
+
> as untrusted input, so it does not blindly implement anything from anyone. Run
|
|
24
|
+
> the loop only on maintainer-approved issues from trusted people, and on an
|
|
25
|
+
> isolated ephemeral runner.
|
|
26
|
+
|
|
27
|
+
## Platform support
|
|
28
|
+
|
|
29
|
+
| Platform | Status | Tested |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| GitLab | supported | end to end on a pilot project |
|
|
32
|
+
| GitHub | experimental | CI templates ship, the issue loop is unverified |
|
|
33
|
+
|
|
34
|
+
## Contents
|
|
35
|
+
|
|
36
|
+
- [Platform support](#platform-support)
|
|
37
|
+
- [The full cycle](#the-full-cycle)
|
|
38
|
+
- [Repository layout](#repository-layout)
|
|
39
|
+
- [Install](#install)
|
|
40
|
+
- [What it costs](#what-it-costs)
|
|
41
|
+
- [Upgrading and removing](#upgrading-and-removing)
|
|
42
|
+
- [The issue-to-code loop (details)](https://gitlab.com/cxi-lmai/ci-agent-platform/-/blob/main/docs/issue-to-code.md)
|
|
43
|
+
- [How it fits together](#how-it-fits-together)
|
|
44
|
+
- [Possible extensions: changing agents and skills](#possible-extensions-changing-agents-and-skills)
|
|
45
|
+
- [Reference: secrets and variables](https://gitlab.com/cxi-lmai/ci-agent-platform/-/blob/main/docs/reference-variables.md)
|
|
46
|
+
|
|
47
|
+
## The full cycle
|
|
48
|
+
|
|
49
|
+
<p align="center">
|
|
50
|
+
<img src="https://gitlab.com/cxi-lmai/ci-agent-platform/-/raw/main/docs/diagrams/full-cycle.svg" alt="The full cycle: from a labeled issue to a merge request ready to merge" width="760">
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
## Repository layout
|
|
54
|
+
|
|
55
|
+
The package has two strictly separated halves. `bin/` is executable and never
|
|
56
|
+
reads or rewrites `payload/`; `payload/` is inert content that is copied and
|
|
57
|
+
never executed by Node. That separation is what lets an upgrade tell an
|
|
58
|
+
untouched file from one the project has edited.
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
.
|
|
62
|
+
├── LICENSE # MIT
|
|
63
|
+
├── README.md # this file, and the npm front page
|
|
64
|
+
├── package.json # bin: ci-agent-platform
|
|
65
|
+
├── bin/init.mjs # phase 0: deterministic, no model
|
|
66
|
+
├── payload/ # everything that ships into your repo
|
|
67
|
+
│ ├── INSTALL.md # instructions for Claude, copied to your root
|
|
68
|
+
│ ├── agents/ # 15 agent definitions (.md)
|
|
69
|
+
│ ├── skills/ # 7 skills (folder with a SKILL.md)
|
|
70
|
+
│ ├── templates/ # 3 templates: config, spec issue, suppressions
|
|
71
|
+
│ └── ci-templates/ # CI jobs and runner scripts (wired by the wizard)
|
|
72
|
+
│ ├── claude-pipeline.gitlab-ci.yml # GitLab CI template
|
|
73
|
+
│ ├── github/ # GitHub Actions workflows
|
|
74
|
+
│ └── scripts/ # shared runner scripts (both platforms)
|
|
75
|
+
├── docs/ # supplementary documentation, not shipped
|
|
76
|
+
│ ├── diagrams/
|
|
77
|
+
│ ├── issue-to-code.md
|
|
78
|
+
│ ├── reference-variables.md
|
|
79
|
+
│ └── superpowers/ # this repository's own plans and specs
|
|
80
|
+
├── examples/unitconv/ # a filled-in example config, not shipped
|
|
81
|
+
└── test/ # this repository's own tests, not shipped
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Install
|
|
85
|
+
|
|
86
|
+
Requires Node 20 or newer, git, and Claude Code. Run both from the root of the
|
|
87
|
+
repository you want to onboard:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx @cxi-lmai/ci-agent-platform # unpacks the platform
|
|
91
|
+
claude # then ask it to install the pipeline
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Two things are worth having before you start, though neither blocks the install.
|
|
95
|
+
A `CLAUDE.md` at the repository root: most agents read it as the project's
|
|
96
|
+
rulebook, and without one the reviewers work from `pipeline-config.md` alone. And
|
|
97
|
+
CI credentials for whichever platform you use, which the wizard walks you
|
|
98
|
+
through at the end. Any one of `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN` or
|
|
99
|
+
`CLAUDE_CODE_OAUTH_TOKEN` authenticates the agents, so a Claude subscription
|
|
100
|
+
works without buying API credit.
|
|
101
|
+
|
|
102
|
+
The first command is plain Node with no model in it. It refuses to run outside
|
|
103
|
+
a git repository, detects your platform from the git remote (without ever
|
|
104
|
+
printing the remote, which may embed a token), unpacks the payload into
|
|
105
|
+
`.ci-agent-platform-src/`, adds that plus two working paths to `.gitignore`,
|
|
106
|
+
writes `INSTALL.md` and a manifest of everything it wrote, and stops. It
|
|
107
|
+
commits nothing, pushes nothing, and asks for no secret.
|
|
108
|
+
|
|
109
|
+
The second hands over to Claude Code, which reads `INSTALL.md` and does the
|
|
110
|
+
rest:
|
|
111
|
+
|
|
112
|
+
1. distributes the files into `.claude/` and `.claude-pipeline/`,
|
|
113
|
+
2. scans the repository and generates `.claude/pipeline-config.md`,
|
|
114
|
+
3. asks whether the issue-to-code loop should run manually or on a schedule,
|
|
115
|
+
and how often,
|
|
116
|
+
4. wires your CI file (`.gitlab-ci.yml`, or a GitHub Actions workflow),
|
|
117
|
+
5. walks you through the tokens one step at a time and verifies each one.
|
|
118
|
+
|
|
119
|
+
> [!NOTE]
|
|
120
|
+
> On a GitHub remote the bootstrapper stops and asks you to confirm that you
|
|
121
|
+
> accept experimental support before it writes anything. Pass
|
|
122
|
+
> `--experimental-github` to answer that in advance, or `--force` to proceed
|
|
123
|
+
> past a prior install it cannot account for.
|
|
124
|
+
|
|
125
|
+
### What it costs
|
|
126
|
+
|
|
127
|
+
Every pipeline job spends paid Claude usage, so the defaults are deliberately
|
|
128
|
+
timid. The issue-to-code loop is **off** until you turn it on (`PIPE_ORCHESTRATE=1`
|
|
129
|
+
on a schedule or a manual run), and `PIPE_CODER_CAP` bounds how many issues one
|
|
130
|
+
orchestrate run hands to the coder, at 3. The review loop runs per merge
|
|
131
|
+
request, and `PIPE_FIX_LOOP_CAP` stops it after 2 bot fix commits rather than
|
|
132
|
+
letting it grind.
|
|
133
|
+
|
|
134
|
+
Turn the schedule on only once both smoke tests pass, and start with a slow
|
|
135
|
+
interval. A frequent schedule against a backlog of ready issues is the one
|
|
136
|
+
configuration that spends real money without anyone watching.
|
|
137
|
+
|
|
138
|
+
### Upgrading and removing
|
|
139
|
+
|
|
140
|
+
Re-run `npx @cxi-lmai/ci-agent-platform` to take a newer release. It records what
|
|
141
|
+
it installed in `.claude/pipeline-install.json`, including a checksum per file,
|
|
142
|
+
so it can tell a file you edited from one it wrote.
|
|
143
|
+
|
|
144
|
+
> [!NOTE]
|
|
145
|
+
> Reconciling those checksums is not implemented yet. Until it is, re-running
|
|
146
|
+
> refreshes the source folder and the manifest; treat updating the installed
|
|
147
|
+
> copies under `.claude/` as a manual diff, and keep your own edits in
|
|
148
|
+
> `pipeline-config.md` rather than in agent files, where nothing will reclaim
|
|
149
|
+
> them.
|
|
150
|
+
|
|
151
|
+
To remove the pipeline: delete `.claude/agents/`, `.claude/skills/`,
|
|
152
|
+
`.claude/templates/`, `.claude/pipeline-config.md`,
|
|
153
|
+
`.claude/pipeline-install.json`, `.claude-pipeline/`, the spec issue template,
|
|
154
|
+
and `.ci-agent-platform-src/`; then drop the `include:` and the `PIPE_*`
|
|
155
|
+
variables from your CI file, and the three `# ci-agent-platform` lines from
|
|
156
|
+
`.gitignore`. Nothing else was touched.
|
|
157
|
+
|
|
158
|
+
> [!IMPORTANT]
|
|
159
|
+
> The installer **pushes only after you confirm** and **must not receive token
|
|
160
|
+
> values in the chat**. It asks permission before every push. You enter secret
|
|
161
|
+
> values yourself in the platform settings (CI/CD variables), so Claude never
|
|
162
|
+
> sees them in the conversation.
|
|
163
|
+
|
|
164
|
+
## How it fits together
|
|
165
|
+
|
|
166
|
+
Each CI job runs `claude "/<skill>"`, and the work splits into two roles:
|
|
167
|
+
|
|
168
|
+
1. **The skill** reads `$PIPE_CONFIG_PATH` and the context the runner prepared,
|
|
169
|
+
does the reasoning (build the diff, choose and run agents, verdict, classify),
|
|
170
|
+
and writes a marker file.
|
|
171
|
+
2. **The runner** does only what needs a token or is mechanical: checkout,
|
|
172
|
+
prepare context from the API, compute the diff base, push commits, post
|
|
173
|
+
comments, set labels, emit metrics, gate.
|
|
174
|
+
|
|
175
|
+
Where to find what:
|
|
176
|
+
|
|
177
|
+
- **Skills** (what each takes as input, what it returns, which keys it writes to
|
|
178
|
+
the marker file): `payload/skills/` here, `.claude/skills/` once installed.
|
|
179
|
+
- **The CI jobs and runner scripts** behind them: `payload/ci-templates/` here,
|
|
180
|
+
`.claude-pipeline/` once installed.
|
|
181
|
+
|
|
182
|
+
## Possible extensions: changing agents and skills
|
|
183
|
+
|
|
184
|
+
The platform is a kit. Adding a capability means adding one definition to your
|
|
185
|
+
own repository. Custom definitions live alongside the installed ones in
|
|
186
|
+
`.claude/`, and the installer never reclaims a file it did not write, so an
|
|
187
|
+
upgrade leaves them alone.
|
|
188
|
+
|
|
189
|
+
### Agent
|
|
190
|
+
|
|
191
|
+
A single `.md` file in `.claude/agents/`. Its front matter
|
|
192
|
+
carries `name`, `description`, `tools`, and `model`, followed by the system
|
|
193
|
+
prompt. Platform convention: the prompt's first step reads `pipeline-config.md`
|
|
194
|
+
(path in `PIPE_CONFIG_PATH`) so the agent takes project specifics from there
|
|
195
|
+
rather than from hardcoded text. Agents are mostly read-only subagents that
|
|
196
|
+
skills call.
|
|
197
|
+
|
|
198
|
+
### Skill
|
|
199
|
+
|
|
200
|
+
A folder with a `SKILL.md` file in `.claude/skills/`. A CI job
|
|
201
|
+
runs it as `claude "/<skill-name>"`. The skill holds the reasoning: it reads the
|
|
202
|
+
config and the prepared context, picks and runs agents, writes the result, and
|
|
203
|
+
emits a marker file for the runner.
|
|
204
|
+
|
|
205
|
+
### How to add one
|
|
206
|
+
|
|
207
|
+
1. Create the definition (`.claude/agents/<name>.md` or
|
|
208
|
+
`.claude/skills/<name>/SKILL.md`), using the installed ones as a template.
|
|
209
|
+
2. Wire it in. A new agent is called by name from a skill (as a subagent), a new
|
|
210
|
+
skill needs a CI job that runs it (see `.claude-pipeline/`).
|
|
211
|
+
3. Commit it. The CI runner does a clean checkout, so an uncommitted definition
|
|
212
|
+
does not exist as far as the pipeline is concerned.
|
|
213
|
+
|
|
214
|
+
> [!NOTE]
|
|
215
|
+
> One project's rules (conventions, domain checks) do not belong in custom
|
|
216
|
+
> agents, but in `pipeline-config.md`, the Domain Checks section, where every
|
|
217
|
+
> agent reads them. A custom agent is for a new capability, not for one
|
|
218
|
+
> repository's specifics. The `agent-architect` agent can propose improvements to
|
|
219
|
+
> the existing agents.
|
package/bin/init.mjs
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Phase 0 of the install: the deterministic half, with no model involved.
|
|
3
|
+
//
|
|
4
|
+
// It unpacks the payload, records what it wrote, and hands over to Claude Code
|
|
5
|
+
// for phases 1 to 6. It deliberately does the least it can: everything that
|
|
6
|
+
// needs judgement about the target repository belongs to the init skill, and
|
|
7
|
+
// everything that needs a secret belongs to the human.
|
|
8
|
+
//
|
|
9
|
+
// This file never reads or rewrites payload/. That separation is what lets the
|
|
10
|
+
// upgrade path treat payload files as content-addressable.
|
|
11
|
+
|
|
12
|
+
import { spawnSync } from 'node:child_process'
|
|
13
|
+
import { createHash } from 'node:crypto'
|
|
14
|
+
import fs from 'node:fs'
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
import readline from 'node:readline/promises'
|
|
17
|
+
import { fileURLToPath } from 'node:url'
|
|
18
|
+
|
|
19
|
+
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
20
|
+
const PAYLOAD = path.join(PKG_ROOT, 'payload')
|
|
21
|
+
const SRC_DIR = '.ci-agent-platform-src'
|
|
22
|
+
const GITIGNORE_ENTRIES = [`${SRC_DIR}/`, '.claude/onboarding-state.md', 'build/pipeline/']
|
|
23
|
+
|
|
24
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'))
|
|
25
|
+
|
|
26
|
+
const argv = process.argv.slice(2)
|
|
27
|
+
const has = (flag) => argv.includes(flag)
|
|
28
|
+
const FORCE = has('--force')
|
|
29
|
+
const YES_GITHUB = has('--experimental-github')
|
|
30
|
+
|
|
31
|
+
function die(message, hint) {
|
|
32
|
+
process.stderr.write(`ci-agent-platform: ${message}\n`)
|
|
33
|
+
if (hint) process.stderr.write(`\n${hint}\n`)
|
|
34
|
+
process.exit(1)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function say(message = '') {
|
|
38
|
+
process.stdout.write(`${message}\n`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (has('--help') || has('-h')) {
|
|
42
|
+
say(`ci-agent-platform ${pkg.version}
|
|
43
|
+
|
|
44
|
+
npx @cxi-lmai/ci-agent-platform unpack into the current repository
|
|
45
|
+
npx @cxi-lmai/ci-agent-platform --force proceed despite an ambiguous prior install
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
--experimental-github accept that GitHub support is experimental, without
|
|
49
|
+
being asked (for non-interactive use)
|
|
50
|
+
--version, --help
|
|
51
|
+
|
|
52
|
+
Phase 0 only. When it finishes, open Claude Code in this repository and ask it
|
|
53
|
+
to install the pipeline; it reads INSTALL.md and runs phases 1 to 6.`)
|
|
54
|
+
process.exit(0)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (has('--version') || has('-v')) {
|
|
58
|
+
say(pkg.version)
|
|
59
|
+
process.exit(0)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --- helpers ---------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
function git(...args) {
|
|
65
|
+
const r = spawnSync('git', args, { encoding: 'utf8' })
|
|
66
|
+
return { ok: r.status === 0, out: (r.stdout || '').trim() }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sha256(file) {
|
|
70
|
+
return `sha256:${createHash('sha256').update(fs.readFileSync(file)).digest('hex')}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Payload-relative paths, sorted, so the manifest is stable across platforms.
|
|
74
|
+
function walk(dir, base = dir) {
|
|
75
|
+
return fs
|
|
76
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
77
|
+
.flatMap((e) => {
|
|
78
|
+
const full = path.join(dir, e.name)
|
|
79
|
+
return e.isDirectory() ? walk(full, base) : [path.relative(base, full)]
|
|
80
|
+
})
|
|
81
|
+
.sort()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Classify the remote host without ever surfacing the URL. A remote can embed
|
|
86
|
+
* an access token in its userinfo, so the URL is read, matched, and dropped:
|
|
87
|
+
* it is never printed, never written to the state file, and never returned.
|
|
88
|
+
*/
|
|
89
|
+
function detectPlatform() {
|
|
90
|
+
const r = git('remote', 'get-url', 'origin')
|
|
91
|
+
if (!r.ok || !r.out) return 'unknown'
|
|
92
|
+
const host = r.out.replace(/\/\/[^/@]+@/, '//')
|
|
93
|
+
if (/gitlab/i.test(host)) return 'gitlab'
|
|
94
|
+
if (/github/i.test(host)) return 'github'
|
|
95
|
+
return 'unknown'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function confirm(question) {
|
|
99
|
+
if (YES_GITHUB) return true
|
|
100
|
+
if (!process.stdin.isTTY) return false
|
|
101
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
102
|
+
try {
|
|
103
|
+
const answer = await rl.question(`${question} [y/N] `)
|
|
104
|
+
return /^y(es)?$/i.test(answer.trim())
|
|
105
|
+
} finally {
|
|
106
|
+
rl.close()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function appendGitignore(repoRoot) {
|
|
111
|
+
const file = path.join(repoRoot, '.gitignore')
|
|
112
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : ''
|
|
113
|
+
const lines = existing.split('\n').map((l) => l.trim())
|
|
114
|
+
const missing = GITIGNORE_ENTRIES.filter((e) => !lines.includes(e))
|
|
115
|
+
if (missing.length === 0) return []
|
|
116
|
+
const prefix = existing === '' || existing.endsWith('\n') ? '' : '\n'
|
|
117
|
+
fs.appendFileSync(file, `${prefix}\n# ci-agent-platform\n${missing.join('\n')}\n`)
|
|
118
|
+
return missing
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- preflight -------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
const top = git('rev-parse', '--show-toplevel')
|
|
124
|
+
if (!top.ok) {
|
|
125
|
+
die(
|
|
126
|
+
'this is not a git repository.',
|
|
127
|
+
'Run it from the root of the repository you want to onboard:\n' +
|
|
128
|
+
' cd /path/to/your/repo && npx @cxi-lmai/ci-agent-platform',
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
const REPO = top.out
|
|
132
|
+
process.chdir(REPO)
|
|
133
|
+
|
|
134
|
+
const configPath = path.join(REPO, '.claude', 'pipeline-config.md')
|
|
135
|
+
const manifestPath = path.join(REPO, '.claude', 'pipeline-install.json')
|
|
136
|
+
if (fs.existsSync(configPath) && !fs.existsSync(manifestPath) && !FORCE) {
|
|
137
|
+
die(
|
|
138
|
+
'found .claude/pipeline-config.md but no .claude/pipeline-install.json.',
|
|
139
|
+
'That combination means an earlier install this tool cannot account for, so\n' +
|
|
140
|
+
'it will not overwrite anything. Re-run with --force once you are satisfied\n' +
|
|
141
|
+
'the existing config can be reconciled by hand.',
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const platform = detectPlatform()
|
|
146
|
+
let githubOptIn = null
|
|
147
|
+
if (platform === 'github') {
|
|
148
|
+
say('GitHub support is experimental.')
|
|
149
|
+
say('The CI workflows ship and are manually verified, but the issue-to-code')
|
|
150
|
+
say('loop carries GitLab-shaped assumptions and is unverified on GitHub.')
|
|
151
|
+
say('GitLab is the supported and tested platform.')
|
|
152
|
+
say()
|
|
153
|
+
githubOptIn = await confirm('Continue anyway?')
|
|
154
|
+
if (!githubOptIn) {
|
|
155
|
+
die(
|
|
156
|
+
'stopped at the GitHub experimental-support check.',
|
|
157
|
+
'Re-run with --experimental-github to accept this without being asked.',
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// --- write -----------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
const srcPath = path.join(REPO, SRC_DIR)
|
|
165
|
+
fs.rmSync(srcPath, { recursive: true, force: true })
|
|
166
|
+
fs.cpSync(PAYLOAD, srcPath, { recursive: true })
|
|
167
|
+
|
|
168
|
+
const addedIgnores = appendGitignore(REPO)
|
|
169
|
+
|
|
170
|
+
// INSTALL.md is copied verbatim: it holds no per-project fact, so there is
|
|
171
|
+
// nothing to template. It is NOT copied over a consumer's own INSTALL.md,
|
|
172
|
+
// which is exactly the class of defect this project shipped once already.
|
|
173
|
+
let installName = 'INSTALL.md'
|
|
174
|
+
const payloadInstall = path.join(PAYLOAD, 'INSTALL.md')
|
|
175
|
+
const rootInstall = path.join(REPO, installName)
|
|
176
|
+
if (fs.existsSync(rootInstall)) {
|
|
177
|
+
const same = fs.readFileSync(rootInstall, 'utf8') === fs.readFileSync(payloadInstall, 'utf8')
|
|
178
|
+
if (!same) installName = 'INSTALL.ci-agent-platform.md'
|
|
179
|
+
}
|
|
180
|
+
fs.copyFileSync(payloadInstall, path.join(REPO, installName))
|
|
181
|
+
|
|
182
|
+
fs.mkdirSync(path.join(REPO, '.claude'), { recursive: true })
|
|
183
|
+
|
|
184
|
+
const payloadFiles = walk(PAYLOAD)
|
|
185
|
+
const manifest = {
|
|
186
|
+
package: pkg.name,
|
|
187
|
+
version: pkg.version,
|
|
188
|
+
platform,
|
|
189
|
+
payload: Object.fromEntries(payloadFiles.map((rel) => [rel, sha256(path.join(PAYLOAD, rel))])),
|
|
190
|
+
}
|
|
191
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
192
|
+
|
|
193
|
+
fs.writeFileSync(
|
|
194
|
+
path.join(REPO, '.claude', 'onboarding-state.md'),
|
|
195
|
+
`# Onboarding state
|
|
196
|
+
|
|
197
|
+
Phase: 0-complete
|
|
198
|
+
Platform: ${platform}
|
|
199
|
+
GitHub experimental opt-in: ${githubOptIn === null ? 'not applicable' : githubOptIn}
|
|
200
|
+
Source folder: ${SRC_DIR}/
|
|
201
|
+
Install instructions: ${installName}
|
|
202
|
+
|
|
203
|
+
Next action: phase 1 of .ci-agent-platform-src/skills/init-pipeline-config/SKILL.md
|
|
204
|
+
(scan the repository, select agents, draft the config).
|
|
205
|
+
|
|
206
|
+
This file is gitignored and is deleted at the end of the install. A session
|
|
207
|
+
resuming an interrupted onboarding reads it first and continues from the phase
|
|
208
|
+
recorded here, following the skill rather than improvising.
|
|
209
|
+
`,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
// --- handoff ---------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
say()
|
|
215
|
+
say(`ci-agent-platform ${pkg.version}: phase 0 complete.`)
|
|
216
|
+
say()
|
|
217
|
+
say(` unpacked ${SRC_DIR}/ (${payloadFiles.length} files)`)
|
|
218
|
+
say(` wrote ${installName}`)
|
|
219
|
+
say(' .claude/pipeline-install.json')
|
|
220
|
+
say(' .claude/onboarding-state.md')
|
|
221
|
+
say(` gitignored ${addedIgnores.length > 0 ? addedIgnores.join(', ') : 'nothing new'}`)
|
|
222
|
+
say(` platform ${platform}${platform === 'github' ? ' (experimental)' : ''}`)
|
|
223
|
+
if (installName !== 'INSTALL.md') {
|
|
224
|
+
say()
|
|
225
|
+
say(' Note: this repository already had its own INSTALL.md, which was left')
|
|
226
|
+
say(` untouched. The install instructions went to ${installName}.`)
|
|
227
|
+
}
|
|
228
|
+
if (platform === 'unknown') {
|
|
229
|
+
say()
|
|
230
|
+
say(' Note: no git remote found, so the platform is unknown. Phase 1 asks.')
|
|
231
|
+
}
|
|
232
|
+
say()
|
|
233
|
+
say('Next: open Claude Code in this repository and ask it to install the')
|
|
234
|
+
say('pipeline (any phrasing). It reads the instructions and takes it from here.')
|
|
235
|
+
say()
|
|
236
|
+
say('Nothing has been committed. Nothing has been pushed.')
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cxi-lmai/ci-agent-platform",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Autonomous dev pipeline on plain GitLab CI or GitHub Actions, driven by Claude Code. A labeled issue goes in, an open merge request comes out.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"ci",
|
|
9
|
+
"gitlab-ci",
|
|
10
|
+
"github-actions",
|
|
11
|
+
"agents",
|
|
12
|
+
"code-review",
|
|
13
|
+
"automation"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://gitlab.com/cxi-lmai/ci-agent-platform#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://gitlab.com/cxi-lmai/ci-agent-platform/-/issues"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://gitlab.com/cxi-lmai/ci-agent-platform.git"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Technical University of Liberec",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"bin": {
|
|
27
|
+
"ci-agent-platform": "bin/init.mjs"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"bin/",
|
|
31
|
+
"payload/",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"test": "npm run test:lint && npm run test:gates && npm run test:unit",
|
|
43
|
+
"test:lint": "shellcheck -S warning test/*.sh payload/ci-templates/scripts/*.sh payload/ci-templates/scripts/lib/*.sh",
|
|
44
|
+
"test:gates": "bash test/coherence-gate.sh all && bash test/restructure-invariants.sh verify",
|
|
45
|
+
"test:unit": "node --test \"test/*.test.mjs\""
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# ci-agent-platform: onboarding
|
|
2
|
+
|
|
3
|
+
Instructions for Claude Code. `npx @cxi-lmai/ci-agent-platform` unpacked the
|
|
4
|
+
platform into this repository, and the user has asked to install it (any
|
|
5
|
+
phrasing: "install ci-agent-platform", "install the pipeline", "set this up").
|
|
6
|
+
Follow this file top to bottom. You are the installer and the guide; the user
|
|
7
|
+
answers questions and approves steps.
|
|
8
|
+
|
|
9
|
+
Ground rules:
|
|
10
|
+
|
|
11
|
+
- Never push and never commit without showing what changed and asking first.
|
|
12
|
+
- Never accept secret values (API keys, tokens) into the conversation. Name
|
|
13
|
+
them, say where to set them, verify presence, not values.
|
|
14
|
+
- Do not overwrite existing files without showing a diff and asking.
|
|
15
|
+
- `.ci-agent-platform-src/` is the source. It is gitignored, so everything the
|
|
16
|
+
CI needs must be copied into tracked paths (step 1): the runner does a clean
|
|
17
|
+
checkout and sees only what the repository tracks.
|
|
18
|
+
|
|
19
|
+
## Step 0: check that the bootstrap ran
|
|
20
|
+
|
|
21
|
+
`.ci-agent-platform-src/` and `.claude/pipeline-install.json` both exist once
|
|
22
|
+
the bootstrapper has run. If either is missing, the user has not run it. Ask
|
|
23
|
+
them to run `npx @cxi-lmai/ci-agent-platform` in this repository, and stop. Do
|
|
24
|
+
not clone the platform yourself and do not improvise a source folder: the
|
|
25
|
+
manifest that the upgrade path depends on is written only by the bootstrapper.
|
|
26
|
+
|
|
27
|
+
Then read `.claude/onboarding-state.md`, which records the detected platform
|
|
28
|
+
and the phase reached.
|
|
29
|
+
|
|
30
|
+
## Step 1: distribute the files
|
|
31
|
+
|
|
32
|
+
Let `<src>` be `.ci-agent-platform-src/`.
|
|
33
|
+
|
|
34
|
+
The platform is `gitlab` or `github`, and `.claude/onboarding-state.md` already
|
|
35
|
+
records which. Use that value. Do not re-read the git remote: the bootstrapper
|
|
36
|
+
detected the platform precisely so that a URL which may embed an access token
|
|
37
|
+
never has to enter this conversation.
|
|
38
|
+
|
|
39
|
+
1. Copy `<src>/agents/` to `.claude/agents/` and `<src>/skills/` to
|
|
40
|
+
`.claude/skills/` (tracked: the CI runner does a clean checkout and needs
|
|
41
|
+
them in the repo).
|
|
42
|
+
|
|
43
|
+
**Never overwrite a file that is already there.** Repositories that qualify
|
|
44
|
+
to install this pipeline are exactly the ones likely to have their own
|
|
45
|
+
`.claude/agents/code-reviewer.md` or `coder.md` already, tuned by hand. For
|
|
46
|
+
every name that collides, show the diff and ask, one file at a time. Do not
|
|
47
|
+
offer "overwrite all". A definition the project wrote outranks the one that
|
|
48
|
+
ships here, and losing it silently is the worst outcome of the whole install.
|
|
49
|
+
2. Copy `<src>/templates/` to `.claude/templates/`, under the same rule.
|
|
50
|
+
3. Create `.claude/memory/review_suppressions.md` from
|
|
51
|
+
`<src>/templates/review_suppressions.template.md` **when it does not already
|
|
52
|
+
exist**, and never touch it when it does: it is a project-owned record.
|
|
53
|
+
The reviewer agents read it on every run, and without the file they read a
|
|
54
|
+
missing path on a repository that has just been told the pipeline is
|
|
55
|
+
installed.
|
|
56
|
+
4. Copy the CI template and runner scripts. The runner scripts go to the same
|
|
57
|
+
place on both platforms, `.claude-pipeline/scripts/`, which is the default
|
|
58
|
+
`PIPE_SCRIPTS_DIR` the CI template already points at. Only the CI definition
|
|
59
|
+
differs:
|
|
60
|
+
- GitLab: `<src>/ci-templates/claude-pipeline.gitlab-ci.yml` to
|
|
61
|
+
`.claude-pipeline/`, and `<src>/ci-templates/scripts/` to
|
|
62
|
+
`.claude-pipeline/scripts/`.
|
|
63
|
+
- GitHub: `<src>/ci-templates/github/*.yml` to `.github/workflows/`, and
|
|
64
|
+
`<src>/ci-templates/scripts/` to `.claude-pipeline/scripts/`. Ask before
|
|
65
|
+
replacing a workflow file that already exists; `claude-pipeline.yml` is an
|
|
66
|
+
ordinary enough name to collide.
|
|
67
|
+
5. Check `.gitignore`. The bootstrapper already added `.ci-agent-platform-src/`,
|
|
68
|
+
`.claude/onboarding-state.md` (the wizard's progress marker, see the skill's
|
|
69
|
+
ground rules) and `build/pipeline/` (the default `PIPE_CONTEXT_DIR`, the
|
|
70
|
+
runner's working files). Add any that are missing. The source folder is a
|
|
71
|
+
copy of the distributed package; the installed files above are what the
|
|
72
|
+
project tracks.
|
|
73
|
+
|
|
74
|
+
Do not commit yet; one commit covers install and configuration together at
|
|
75
|
+
the end of step 2.
|
|
76
|
+
|
|
77
|
+
When you reach that commit, do not stage the copy of this file at the
|
|
78
|
+
repository root (`INSTALL.md`, or `INSTALL.ci-agent-platform.md` where the
|
|
79
|
+
project had its own). It is an instruction sheet for you, not documentation for
|
|
80
|
+
the project, and it reads as the latter once committed. Delete it together with
|
|
81
|
+
`.ci-agent-platform-src/` at the end of the install, as you already do with
|
|
82
|
+
`.claude/onboarding-state.md`. Everything either file was needed for is by then
|
|
83
|
+
recorded in `.claude/pipeline-install.json`, and re-running the bootstrapper
|
|
84
|
+
restores both.
|
|
85
|
+
|
|
86
|
+
## Step 2: run the configuration wizard
|
|
87
|
+
|
|
88
|
+
Follow `.claude/skills/init-pipeline-config/SKILL.md` from phase 1 (it is now
|
|
89
|
+
in the repo; in a fresh session it is also available as the
|
|
90
|
+
`/init-pipeline-config` skill). It scans the repo, generates
|
|
91
|
+
`.claude/pipeline-config.md`, asks the user only what it cannot detect, wires
|
|
92
|
+
the project CI file, guides the secrets setup one step at a time (with a
|
|
93
|
+
step counter), runs the smoke test itself, and closes with one recap table
|
|
94
|
+
of every decision.
|
|
95
|
+
|
|
96
|
+
Do not duplicate its content here. One rule binds both files: the user does
|
|
97
|
+
only what a human must (tokens, CI variables), you do everything else. Keep
|
|
98
|
+
the ceremony minimal: no per-step approvals, one commit at the end. Ask only
|
|
99
|
+
when something is destructive, secret-related, or about to be pushed.
|
|
100
|
+
|
|
101
|
+
Once running, a ready issue must carry its full spec in the issue description,
|
|
102
|
+
not in a comment: the orchestrator reads only the description, and a spec left
|
|
103
|
+
in a comment triages as missing sections.
|
|
104
|
+
|
|
105
|
+
## Resuming an interrupted onboarding
|
|
106
|
+
|
|
107
|
+
When a new session is asked to continue an installation ("continue", "resume",
|
|
108
|
+
any phrasing, in any language), do not improvise from the repo state. Read
|
|
109
|
+
`.claude/onboarding-state.md` (the wizard maintains it), then re-read
|
|
110
|
+
`.claude/skills/init-pipeline-config/SKILL.md`, and continue at the recorded
|
|
111
|
+
phase, following the skill to the letter. Completed phases are never redone.
|
|
112
|
+
When the state file is missing, infer the phase from the repo (installed
|
|
113
|
+
files, config, git log) and still follow the skill, not your own plan.
|