carson 2.6.0 → 2.8.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.
- checksums.yaml +4 -4
- data/.github/workflows/carson_policy.yml +0 -7
- data/API.md +3 -2
- data/MANUAL.md +13 -2
- data/README.md +1 -1
- data/RELEASE.md +133 -12
- data/VERSION +1 -1
- data/carson.gemspec +12 -0
- data/lib/carson/cli.rb +3 -1
- data/lib/carson/config.rb +3 -2
- data/lib/carson/runtime/audit.rb +3 -11
- data/lib/carson/runtime/local.rb +17 -17
- data/lib/carson/runtime/setup.rb +295 -0
- data/lib/carson/runtime.rb +4 -2
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c3f3bf2f0ea58a2d460c36fb57b6313d5dab97c08b76ec94ae1c1e9b685b24f
|
|
4
|
+
data.tar.gz: 48562197450233540c28a6fa912c38eaa636910ed3bab57e32e95d7b0b66c10a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 63a4d9fed728d78b2fcaf0d0d6ffb158a93e464af24997d661b24d77a2764e4a84014f45d4cfe2e204c7bac337c796f8e69e3722fe30a135f98650176271dba3
|
|
7
|
+
data.tar.gz: aba034301bccbf786198a1a44bd767d3f239370807a68841a89b6ffc40340719ef37dea2ddae9a0d54eedadf1b42c2446d02614721ab946586b809de0438a2d4
|
|
@@ -66,13 +66,6 @@ jobs:
|
|
|
66
66
|
- name: Install pinned RuboCop
|
|
67
67
|
run: gem install rubocop -v "${{ inputs.rubocop_version }}" --no-document
|
|
68
68
|
|
|
69
|
-
- name: Align remote name for Carson
|
|
70
|
-
working-directory: host
|
|
71
|
-
run: |
|
|
72
|
-
if ! git remote get-url github >/dev/null 2>&1; then
|
|
73
|
-
git remote rename origin github
|
|
74
|
-
fi
|
|
75
|
-
|
|
76
69
|
- name: Carson audit
|
|
77
70
|
working-directory: host
|
|
78
71
|
env:
|
data/API.md
CHANGED
|
@@ -15,8 +15,9 @@ carson <command> [subcommand] [arguments]
|
|
|
15
15
|
|
|
16
16
|
| Command | Purpose |
|
|
17
17
|
|---|---|
|
|
18
|
+
| `carson setup` | Interactive quiz to configure remote, main branch, workflow, and merge method. Writes `~/.carson/config.json`. |
|
|
18
19
|
| `carson lint setup --source <path-or-git-url> [--ref <git-ref>] [--force]` | Seed or refresh `~/.carson/lint` policy files from an explicit source. |
|
|
19
|
-
| `carson onboard [repo_path]` | Apply one-command baseline setup for a target git repository. |
|
|
20
|
+
| `carson onboard [repo_path]` | Apply one-command baseline setup for a target git repository. Auto-triggers `setup` on first run. |
|
|
20
21
|
| `carson prepare` | Install or refresh Carson-managed global hooks. |
|
|
21
22
|
| `carson refresh [repo_path]` | Re-apply hooks, templates, and audit after upgrading Carson. |
|
|
22
23
|
| `carson offboard [repo_path]` | Remove Carson-managed host artefacts and detach Carson hooks path where applicable. |
|
|
@@ -40,7 +41,7 @@ carson <command> [subcommand] [arguments]
|
|
|
40
41
|
|
|
41
42
|
`--loop SECONDS` runs the govern cycle continuously, sleeping SECONDS between cycles. The loop isolates errors per cycle — a single failing cycle does not stop the daemon. `Ctrl-C` cleanly exits with a cycle count summary. SECONDS must be a positive integer.
|
|
42
43
|
|
|
43
|
-
`govern.merge.method` accepts `squash`, `merge`, or `rebase` (default: `squash`). Squash keeps main linear — one PR, one commit. When the target repository enforces linear history via branch protection,
|
|
44
|
+
`govern.merge.method` accepts `squash`, `merge`, or `rebase` (default: `squash`). Squash keeps main linear — one PR, one commit. When the target repository enforces linear history via branch protection, both `squash` and `rebase` are accepted by GitHub — only `merge` is rejected.
|
|
44
45
|
|
|
45
46
|
### Review commands
|
|
46
47
|
|
data/MANUAL.md
CHANGED
|
@@ -50,14 +50,25 @@ Policy layout: language config files sit directly under `CODING/` (flat layout,
|
|
|
50
50
|
carson onboard /path/to/your-repo
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
On first run (no `~/.carson/config.json` exists), `onboard` launches `carson setup` — an interactive quiz that detects your remotes, main branch, and preferred workflow. In non-interactive environments (CI, pipes), Carson auto-detects settings silently.
|
|
54
|
+
|
|
53
55
|
`onboard` performs:
|
|
54
|
-
-
|
|
56
|
+
- Interactive setup quiz (first run only).
|
|
57
|
+
- Remote detection and verification using configured `git.remote` (default `origin`).
|
|
55
58
|
- Hook installation under `~/.carson/hooks/<version>/`.
|
|
56
59
|
- Repository `core.hooksPath` alignment to Carson global hooks.
|
|
57
60
|
- Commit-time governance gate via managed `pre-commit` hook.
|
|
58
61
|
- Managed `.github/*` template synchronisation.
|
|
59
62
|
- Initial governance audit.
|
|
60
63
|
|
|
64
|
+
### Reconfigure later
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
carson setup
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Re-run the interactive setup quiz to change your remote, main branch, workflow style, or merge method. Choices are saved to `~/.carson/config.json`.
|
|
71
|
+
|
|
61
72
|
### Step 3: Commit generated files
|
|
62
73
|
|
|
63
74
|
After `onboard`, commit the generated `.github/*` changes in your repository. From this point the repository is governed.
|
|
@@ -160,7 +171,7 @@ Carson's `govern.merge.method` controls how `carson govern` merges ready PRs. Th
|
|
|
160
171
|
|
|
161
172
|
**When to use other methods:**
|
|
162
173
|
|
|
163
|
-
- `rebase` — if you want to preserve individual commits from the branch on main.
|
|
174
|
+
- `rebase` — if you want to preserve individual commits from the branch on main. Both `squash` and `rebase` are compatible with GitHub's "Require linear history" branch protection — only `merge` is rejected.
|
|
164
175
|
- `merge` — if you want explicit merge commits. This creates a non-linear graph but preserves branch topology.
|
|
165
176
|
|
|
166
177
|
**Important:** Carson's merge method must match your GitHub repository's allowed merge types. If your repo only allows squash merges and Carson is set to `merge`, govern will fail when it tries to auto-merge. Check your repository settings under Settings > General > Pull Requests.
|
data/README.md
CHANGED
|
@@ -43,7 +43,7 @@ This separation is Carson's defining trait — the **outsider boundary**: no Car
|
|
|
43
43
|
The data flow:
|
|
44
44
|
|
|
45
45
|
1. You maintain a **policy source** — a directory or git repository containing your lint rules (e.g. `CODING/rubocop.yml`). Carson copies these to `~/.carson/lint/` via `carson lint setup`.
|
|
46
|
-
2. `carson
|
|
46
|
+
2. `carson onboard` installs git hooks, synchronises `.github/*` templates, and runs a first governance audit on a host repository.
|
|
47
47
|
3. From that point, every commit triggers `carson audit` through the managed `pre-commit` hook. The same `carson audit` runs in GitHub Actions. If it passes locally, it passes in CI.
|
|
48
48
|
4. `carson review gate` enforces review accountability: it blocks merge until every actionable reviewer comment has been formally acknowledged by the PR author through a **disposition comment**.
|
|
49
49
|
5. `carson govern` triages all open PRs across your portfolio. Ready PRs are merged and housekept. Failing PRs get a coding agent dispatched to fix them. Stuck PRs are escalated for your attention.
|
data/RELEASE.md
CHANGED
|
@@ -5,25 +5,148 @@ Release-note scope rule:
|
|
|
5
5
|
- `RELEASE.md` records only version deltas, breaking changes, and migration actions.
|
|
6
6
|
- Operational usage guides live in `MANUAL.md` and `API.md`.
|
|
7
7
|
|
|
8
|
-
## 2.
|
|
8
|
+
## 2.8.0 — Interactive Setup and Remote Detection
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- **`carson setup` command.** An interactive quiz that detects git remotes, main branch, workflow style, and merge method. Writes answers to `~/.carson/config.json`. In non-TTY environments, Carson auto-detects settings silently.
|
|
13
|
+
- **Auto-triggered on first onboard.** `carson onboard` now launches the setup quiz when no `~/.carson/config.json` exists. Existing users are not affected.
|
|
14
|
+
- **Remote renaming removed.** Carson no longer renames `origin` to `github` during onboard. Instead, it detects the existing remote and adapts. This respects the user's repository layout.
|
|
15
|
+
- **Default remote changed from `github` to `origin`.** The built-in default `git.remote` is now `origin`, matching the convention of most git hosting providers. Users who previously relied on the `github` default should run `carson setup` or set `git.remote` in config.
|
|
16
|
+
- **Post-install message.** `gem install carson` now displays a getting-started guide pointing to `carson onboard`.
|
|
17
|
+
- **CI lint fallback simplified.** `lint_target_files_for_pull_request` uses `config.git_remote` directly with a minimal fallback to `origin`.
|
|
18
|
+
|
|
19
|
+
### What users must do now
|
|
20
|
+
|
|
21
|
+
1. Upgrade Carson to `2.8.0`.
|
|
22
|
+
2. If you relied on the `github` remote default, either rename your remote to `origin` or run `carson setup` to configure `git.remote`.
|
|
23
|
+
|
|
24
|
+
### Breaking or removed behaviour
|
|
25
|
+
|
|
26
|
+
- `git.remote` default changed from `github` to `origin`.
|
|
27
|
+
- `carson onboard` no longer renames `origin` to `github`.
|
|
28
|
+
- The `align_remote_name_for_carson!` method has been removed.
|
|
29
|
+
|
|
30
|
+
### Upgrade steps
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
cd ~/Dev/carson
|
|
34
|
+
git pull
|
|
35
|
+
bash install.sh
|
|
36
|
+
carson version
|
|
37
|
+
carson setup
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Engineering Appendix
|
|
41
|
+
|
|
42
|
+
#### New files
|
|
43
|
+
|
|
44
|
+
- `lib/carson/runtime/setup.rb` — interactive quiz, remote/branch detection, config persistence.
|
|
45
|
+
- `test/runtime_setup_test.rb` — quiz, detection, and config merge tests.
|
|
46
|
+
|
|
47
|
+
#### Modified components
|
|
48
|
+
|
|
49
|
+
- `lib/carson/config.rb` — default `git.remote` changed from `"github"` to `"origin"`, added `attr_accessor :git_remote`.
|
|
50
|
+
- `lib/carson/runtime.rb` — added `in_stream:` parameter and `@in` attribute.
|
|
51
|
+
- `lib/carson/cli.rb` — added `"setup"` command dispatch, updated banner.
|
|
52
|
+
- `lib/carson/runtime/local.rb` — replaced `align_remote_name_for_carson!` with `report_detected_remote!`, updated `onboard!` to auto-trigger setup, updated `print_onboarding_guidance`.
|
|
53
|
+
- `lib/carson/runtime/audit.rb` — simplified `lint_target_files_for_pull_request` CI fallback.
|
|
54
|
+
- `test/runtime_govern_test.rb` — removed `origin` → `github` rename.
|
|
55
|
+
- `test/runtime_audit_lint_test.rb` — updated remote name from `github` to `origin`.
|
|
56
|
+
- `carson.gemspec` — added `spec.post_install_message`.
|
|
57
|
+
- `MANUAL.md` — documented `carson setup`, updated remote default.
|
|
58
|
+
- `API.md` — added `setup` command entry.
|
|
59
|
+
|
|
60
|
+
#### Public interface and config changes
|
|
61
|
+
|
|
62
|
+
- Added CLI command: `carson setup`.
|
|
63
|
+
- Default `git.remote` changed from `"github"` to `"origin"`.
|
|
64
|
+
- Runtime constructor accepts `in_stream:` keyword argument.
|
|
65
|
+
- Exit status contract unchanged.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 2.7.0 — Documentation and Test Fixes
|
|
70
|
+
|
|
71
|
+
### What changed
|
|
72
|
+
|
|
73
|
+
- **Stale command reference fixed.** README.md referenced the pre-2.3.0 command name `carson init` instead of `carson onboard`.
|
|
74
|
+
- **Linear history guidance corrected.** API.md and MANUAL.md incorrectly stated that GitHub's "Require linear history" only accepts rebase merges. Both squash and rebase are accepted — only merge commits are rejected.
|
|
75
|
+
- **Release notes separated.** The combined 2.6.0 entry has been split into distinct 2.5.0 (agent discovery) and 2.6.0 (squash default) entries.
|
|
76
|
+
- **Config default test made hermetic.** `test_config_govern_defaults` now isolates HOME to a temp directory, preventing the developer's local `~/.carson/config.json` from affecting test results.
|
|
77
|
+
|
|
78
|
+
### What users must do now
|
|
79
|
+
|
|
80
|
+
1. Upgrade Carson to `2.7.0`.
|
|
81
|
+
|
|
82
|
+
### Breaking or removed behaviour
|
|
83
|
+
|
|
84
|
+
- None.
|
|
85
|
+
|
|
86
|
+
### Upgrade steps
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
cd ~/Dev/carson
|
|
90
|
+
git pull
|
|
91
|
+
bash install.sh
|
|
92
|
+
carson version
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 2.6.0 — Default Squash Merge
|
|
9
98
|
|
|
10
99
|
### What changed
|
|
11
100
|
|
|
12
101
|
- **Default merge method changed from `merge` to `squash`.** Squash-to-main keeps history linear: one PR = one commit on main. Every commit on main corresponds to a reviewed, CI-passing unit of work and is individually revertable. This aligns Carson's built-in default with how most teams should run.
|
|
102
|
+
|
|
103
|
+
### What users must do now
|
|
104
|
+
|
|
105
|
+
1. Upgrade Carson to `2.6.0`.
|
|
106
|
+
2. If you previously set `govern.merge.method` to `"merge"` explicitly in `~/.carson/config.json`, review whether `"squash"` (now the default) is the right choice.
|
|
107
|
+
|
|
108
|
+
### Breaking or removed behaviour
|
|
109
|
+
|
|
110
|
+
- `govern.merge.method` default changed from `merge` to `squash`. If your GitHub repository only allows merge commits, set `"govern": { "merge": { "method": "merge" } }` in `~/.carson/config.json`.
|
|
111
|
+
|
|
112
|
+
### Upgrade steps
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
cd ~/Dev/carson
|
|
116
|
+
git pull
|
|
117
|
+
bash install.sh
|
|
118
|
+
carson version
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Engineering Appendix
|
|
122
|
+
|
|
123
|
+
#### Modified components
|
|
124
|
+
|
|
125
|
+
- `lib/carson/config.rb` — `govern.merge.method` default changed from `"merge"` to `"squash"`.
|
|
126
|
+
- `test/runtime_govern_test.rb` — unit test updated for squash default.
|
|
127
|
+
|
|
128
|
+
#### Verification evidence
|
|
129
|
+
|
|
130
|
+
- CI passes on PR #78.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 2.5.0 — Agent Discovery Templates
|
|
135
|
+
|
|
136
|
+
### What changed
|
|
137
|
+
|
|
13
138
|
- **Agent discovery via managed templates.** Interactive agents (Claude Code, Codex, Copilot) working in Carson-governed repos now discover Carson automatically. A new source-of-truth file `.github/carson-instructions.md` contains the full governance baseline. Agent-specific files (`.github/CLAUDE.md`, `.github/AGENTS.md`, `.github/copilot-instructions.md`) are one-line pointers to it. Zero drift risk — one file to maintain, all agents follow the same reference.
|
|
14
139
|
- **Managed template set expanded.** `carson template apply` now writes five files: `carson-instructions.md`, `copilot-instructions.md`, `CLAUDE.md`, `AGENTS.md`, and `pull_request_template.md`.
|
|
15
140
|
|
|
16
141
|
### What users must do now
|
|
17
142
|
|
|
18
|
-
1. Upgrade Carson to `2.
|
|
143
|
+
1. Upgrade Carson to `2.5.0`.
|
|
19
144
|
2. Run `carson prepare` in each governed repository.
|
|
20
145
|
3. Run `carson template apply` to write the new managed files.
|
|
21
146
|
4. Commit the new `.github/*` files.
|
|
22
|
-
5. If you previously set `govern.merge.method` to `"merge"` explicitly in `~/.carson/config.json`, review whether `"squash"` (now the default) is the right choice.
|
|
23
147
|
|
|
24
148
|
### Breaking or removed behaviour
|
|
25
149
|
|
|
26
|
-
- `govern.merge.method` default changed from `merge` to `squash`. If your GitHub repository only allows merge commits, set `"govern": { "merge": { "method": "merge" } }` in `~/.carson/config.json`.
|
|
27
150
|
- `.github/copilot-instructions.md` content replaced with a one-line reference. The governance baseline now lives in `.github/carson-instructions.md`.
|
|
28
151
|
|
|
29
152
|
### Upgrade steps
|
|
@@ -39,12 +162,6 @@ carson template apply
|
|
|
39
162
|
|
|
40
163
|
### Engineering Appendix
|
|
41
164
|
|
|
42
|
-
#### Modified components
|
|
43
|
-
|
|
44
|
-
- `lib/carson/config.rb` — `govern.merge.method` default changed from `"merge"` to `"squash"`; `template.managed_files` expanded to include `carson-instructions.md`, `CLAUDE.md`, and `AGENTS.md`.
|
|
45
|
-
- `script/ci_smoke.sh` — offboard removal check updated for new managed files.
|
|
46
|
-
- `test/runtime_govern_test.rb` — unit test updated for squash default.
|
|
47
|
-
|
|
48
165
|
#### New files
|
|
49
166
|
|
|
50
167
|
- `templates/.github/carson-instructions.md` — governance baseline source of truth.
|
|
@@ -55,15 +172,19 @@ carson template apply
|
|
|
55
172
|
|
|
56
173
|
- `templates/.github/copilot-instructions.md` — replaced full content with one-line reference.
|
|
57
174
|
|
|
175
|
+
#### Modified components
|
|
176
|
+
|
|
177
|
+
- `lib/carson/config.rb` — `template.managed_files` expanded to include `carson-instructions.md`, `CLAUDE.md`, and `AGENTS.md`.
|
|
178
|
+
- `script/ci_smoke.sh` — offboard removal check updated for new managed files.
|
|
179
|
+
|
|
58
180
|
#### Public interface and config changes
|
|
59
181
|
|
|
60
|
-
- `govern.merge.method` default: `"merge"` → `"squash"`.
|
|
61
182
|
- `template.managed_files` default expanded from 2 to 5 files.
|
|
62
183
|
- Exit status contract unchanged.
|
|
63
184
|
|
|
64
185
|
#### Verification evidence
|
|
65
186
|
|
|
66
|
-
- CI passes on
|
|
187
|
+
- CI passes on PR #77.
|
|
67
188
|
|
|
68
189
|
---
|
|
69
190
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.8.0
|
data/carson.gemspec
CHANGED
|
@@ -19,6 +19,18 @@ Gem::Specification.new do |spec|
|
|
|
19
19
|
"documentation_uri" => "https://github.com/wanghailei/carson/blob/main/MANUAL.md"
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
spec.post_install_message = <<~MSG
|
|
23
|
+
|
|
24
|
+
\u29D3 Carson at your service.
|
|
25
|
+
|
|
26
|
+
Step into your project directory and run:
|
|
27
|
+
|
|
28
|
+
carson onboard
|
|
29
|
+
|
|
30
|
+
I'll walk you through everything from there.
|
|
31
|
+
|
|
32
|
+
MSG
|
|
33
|
+
|
|
22
34
|
spec.bindir = "exe"
|
|
23
35
|
spec.executables = [ "carson" ]
|
|
24
36
|
spec.require_paths = [ "lib" ]
|
data/lib/carson/cli.rb
CHANGED
|
@@ -44,7 +44,7 @@ module Carson
|
|
|
44
44
|
|
|
45
45
|
def self.build_parser
|
|
46
46
|
OptionParser.new do |opts|
|
|
47
|
-
opts.banner = "Usage: carson [audit|sync|prune|prepare|inspect|onboard [repo_path]|refresh [repo_path]|offboard [repo_path]|template check|template apply|lint setup --source <path-or-git-url>|review gate|review sweep|govern [--dry-run] [--json] [--loop SECONDS]|housekeep|version]"
|
|
47
|
+
opts.banner = "Usage: carson [setup|audit|sync|prune|prepare|inspect|onboard [repo_path]|refresh [repo_path]|offboard [repo_path]|template check|template apply|lint setup --source <path-or-git-url>|review gate|review sweep|govern [--dry-run] [--json] [--loop SECONDS]|housekeep|version]"
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
|
@@ -187,6 +187,8 @@ module Carson
|
|
|
187
187
|
return Runtime::EXIT_ERROR if command == :invalid
|
|
188
188
|
|
|
189
189
|
case command
|
|
190
|
+
when "setup"
|
|
191
|
+
runtime.setup!
|
|
190
192
|
when "audit"
|
|
191
193
|
runtime.audit!
|
|
192
194
|
when "sync"
|
data/lib/carson/config.rb
CHANGED
|
@@ -6,7 +6,8 @@ module Carson
|
|
|
6
6
|
|
|
7
7
|
# Config is built-in only for outsider mode; host repositories do not carry Carson config files.
|
|
8
8
|
class Config
|
|
9
|
-
|
|
9
|
+
attr_accessor :git_remote
|
|
10
|
+
attr_reader :main_branch, :protected_branches, :hooks_base_path, :required_hooks,
|
|
10
11
|
:path_groups, :template_managed_files, :lint_languages,
|
|
11
12
|
:review_wait_seconds, :review_poll_seconds, :review_max_polls, :review_sweep_window_days,
|
|
12
13
|
:review_sweep_states, :review_disposition_prefix, :review_risk_keywords,
|
|
@@ -28,7 +29,7 @@ module Carson
|
|
|
28
29
|
def self.default_data
|
|
29
30
|
{
|
|
30
31
|
"git" => {
|
|
31
|
-
"remote" => "
|
|
32
|
+
"remote" => "origin",
|
|
32
33
|
"main_branch" => "main",
|
|
33
34
|
"protected_branches" => [ "main", "master" ]
|
|
34
35
|
},
|
data/lib/carson/runtime/audit.rb
CHANGED
|
@@ -205,19 +205,11 @@ module Carson
|
|
|
205
205
|
base_ref = ENV.fetch( "GITHUB_BASE_REF", "" ).to_s.strip
|
|
206
206
|
return nil if base_ref.empty?
|
|
207
207
|
|
|
208
|
-
|
|
209
|
-
remote_name
|
|
210
|
-
|
|
211
|
-
remotes_stdout, _, remotes_success, = git_run( "remote" )
|
|
212
|
-
if remotes_success
|
|
213
|
-
available_remotes = remotes_stdout.lines.map { |line| line.to_s.strip }.reject( &:empty? )
|
|
214
|
-
candidates = [ preferred_remote, "origin", "github" ].map( &:to_s ).map( &:strip ).reject( &:empty? ).uniq
|
|
215
|
-
remote_name = candidates.find { |candidate| available_remotes.include?( candidate ) }
|
|
216
|
-
remote_name ||= available_remotes.first unless available_remotes.empty?
|
|
208
|
+
remote_name = config.git_remote
|
|
209
|
+
unless git_remote_exists?( remote_name: remote_name )
|
|
210
|
+
remote_name = "origin" if git_remote_exists?( remote_name: "origin" )
|
|
217
211
|
end
|
|
218
212
|
|
|
219
|
-
remote_name ||= ( preferred_remote.empty? ? "origin" : preferred_remote )
|
|
220
|
-
|
|
221
213
|
_, _, fetch_success, = git_run( "fetch", "--no-tags", "--depth", "1", remote_name, base_ref )
|
|
222
214
|
return nil unless fetch_success
|
|
223
215
|
|
data/lib/carson/runtime/local.rb
CHANGED
|
@@ -161,7 +161,7 @@ module Carson
|
|
|
161
161
|
inspect!
|
|
162
162
|
end
|
|
163
163
|
|
|
164
|
-
# One-command onboarding for new repositories:
|
|
164
|
+
# One-command onboarding for new repositories: detect remote, install hooks,
|
|
165
165
|
# apply templates, and produce a first audit report.
|
|
166
166
|
def onboard!
|
|
167
167
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
@@ -172,7 +172,17 @@ module Carson
|
|
|
172
172
|
puts_line "ERROR: #{repo_root} is not a git repository."
|
|
173
173
|
return EXIT_ERROR
|
|
174
174
|
end
|
|
175
|
-
|
|
175
|
+
|
|
176
|
+
unless global_config_exists?
|
|
177
|
+
if self.in.respond_to?( :tty? ) && self.in.tty?
|
|
178
|
+
setup_status = setup!
|
|
179
|
+
return setup_status unless setup_status == EXIT_OK
|
|
180
|
+
else
|
|
181
|
+
silent_setup!
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
report_detected_remote!
|
|
176
186
|
hook_status = prepare!
|
|
177
187
|
return hook_status unless hook_status == EXIT_OK
|
|
178
188
|
|
|
@@ -661,30 +671,20 @@ module Carson
|
|
|
661
671
|
end
|
|
662
672
|
end
|
|
663
673
|
|
|
664
|
-
#
|
|
665
|
-
|
|
666
|
-
def align_remote_name_for_carson!
|
|
674
|
+
# Verifies configured remote exists and logs status without mutating remotes.
|
|
675
|
+
def report_detected_remote!
|
|
667
676
|
if git_remote_exists?( remote_name: config.git_remote )
|
|
668
677
|
puts_line "remote_ok: #{config.git_remote}"
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
if git_remote_exists?( remote_name: "origin" )
|
|
672
|
-
git_system!( "remote", "rename", "origin", config.git_remote )
|
|
673
|
-
puts_line "remote_renamed: origin -> #{config.git_remote}"
|
|
674
|
-
return
|
|
678
|
+
else
|
|
679
|
+
puts_line "WARN: remote '#{config.git_remote}' not found; run carson setup to configure."
|
|
675
680
|
end
|
|
676
|
-
puts_line "WARN: no #{config.git_remote} or origin remote configured; continue with local baseline only."
|
|
677
681
|
end
|
|
678
682
|
|
|
679
|
-
|
|
680
683
|
def print_onboarding_guidance
|
|
681
684
|
puts_line ""
|
|
682
685
|
puts_line "Carson is ready. Current workflow: #{config.workflow_style}"
|
|
683
686
|
puts_line ""
|
|
684
|
-
puts_line "
|
|
685
|
-
puts_line " { \"workflow\": { \"style\": \"branch\" } } — enforce PR-only merges"
|
|
686
|
-
puts_line " { \"workflow\": { \"style\": \"trunk\" } } — allow direct main commits (default)"
|
|
687
|
-
puts_line ""
|
|
687
|
+
puts_line "Reconfigure anytime with: carson setup"
|
|
688
688
|
puts_line "Run carson refresh after changing config."
|
|
689
689
|
end
|
|
690
690
|
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
module Carson
|
|
2
|
+
class Runtime
|
|
3
|
+
module Setup
|
|
4
|
+
WELL_KNOWN_REMOTES = %w[origin github upstream].freeze
|
|
5
|
+
|
|
6
|
+
def setup!
|
|
7
|
+
print_header "Setup"
|
|
8
|
+
|
|
9
|
+
unless inside_git_work_tree?
|
|
10
|
+
puts_line "WARN: not a git repository. Skipping remote and branch detection."
|
|
11
|
+
return write_setup_config( choices: {} )
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
if self.in.respond_to?( :tty? ) && self.in.tty?
|
|
15
|
+
interactive_setup!
|
|
16
|
+
else
|
|
17
|
+
silent_setup!
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def interactive_setup!
|
|
24
|
+
choices = {}
|
|
25
|
+
|
|
26
|
+
remote_choice = prompt_remote
|
|
27
|
+
choices[ "git.remote" ] = remote_choice unless remote_choice.nil?
|
|
28
|
+
|
|
29
|
+
branch_choice = prompt_main_branch
|
|
30
|
+
choices[ "git.main_branch" ] = branch_choice unless branch_choice.nil?
|
|
31
|
+
|
|
32
|
+
workflow_choice = prompt_workflow_style
|
|
33
|
+
choices[ "workflow.style" ] = workflow_choice unless workflow_choice.nil?
|
|
34
|
+
|
|
35
|
+
merge_choice = prompt_merge_method
|
|
36
|
+
choices[ "govern.merge.method" ] = merge_choice unless merge_choice.nil?
|
|
37
|
+
|
|
38
|
+
write_setup_config( choices: choices )
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def silent_setup!
|
|
42
|
+
detected = detect_git_remote
|
|
43
|
+
choices = {}
|
|
44
|
+
if detected && detected != config.git_remote
|
|
45
|
+
choices[ "git.remote" ] = detected
|
|
46
|
+
puts_line "detected_remote: #{detected}"
|
|
47
|
+
elsif detected
|
|
48
|
+
puts_line "detected_remote: #{detected}"
|
|
49
|
+
else
|
|
50
|
+
puts_line "detected_remote: none"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
branch = detect_main_branch
|
|
54
|
+
if branch && branch != config.main_branch
|
|
55
|
+
choices[ "git.main_branch" ] = branch
|
|
56
|
+
puts_line "detected_main_branch: #{branch}"
|
|
57
|
+
elsif branch
|
|
58
|
+
puts_line "detected_main_branch: #{branch}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
write_setup_config( choices: choices )
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def prompt_remote
|
|
65
|
+
remotes = list_git_remotes
|
|
66
|
+
if remotes.empty?
|
|
67
|
+
puts_line "No remotes found. Carson will operate in local-only mode."
|
|
68
|
+
return nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
puts_line ""
|
|
72
|
+
puts_line "Git remote"
|
|
73
|
+
options = build_remote_options( remotes: remotes )
|
|
74
|
+
options << { label: "Other (enter name)", value: :other }
|
|
75
|
+
|
|
76
|
+
default_index = 0
|
|
77
|
+
choice = prompt_choice( options: options, default: default_index )
|
|
78
|
+
|
|
79
|
+
if choice == :other
|
|
80
|
+
prompt_custom_value( label: "Remote name" )
|
|
81
|
+
else
|
|
82
|
+
choice
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def prompt_main_branch
|
|
87
|
+
puts_line ""
|
|
88
|
+
puts_line "Main branch"
|
|
89
|
+
options = build_main_branch_options
|
|
90
|
+
options << { label: "Other (enter name)", value: :other }
|
|
91
|
+
|
|
92
|
+
default_index = 0
|
|
93
|
+
choice = prompt_choice( options: options, default: default_index )
|
|
94
|
+
|
|
95
|
+
if choice == :other
|
|
96
|
+
prompt_custom_value( label: "Branch name" )
|
|
97
|
+
else
|
|
98
|
+
choice
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def prompt_workflow_style
|
|
103
|
+
puts_line ""
|
|
104
|
+
puts_line "Workflow style"
|
|
105
|
+
options = [
|
|
106
|
+
{ label: "trunk — commit directly to main (default)", value: "trunk" },
|
|
107
|
+
{ label: "branch — enforce PR-only merges", value: "branch" }
|
|
108
|
+
]
|
|
109
|
+
prompt_choice( options: options, default: 0 )
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def prompt_merge_method
|
|
113
|
+
puts_line ""
|
|
114
|
+
puts_line "Merge method"
|
|
115
|
+
options = [
|
|
116
|
+
{ label: "squash — one commit per PR (recommended)", value: "squash" },
|
|
117
|
+
{ label: "rebase — linear history, individual commits", value: "rebase" },
|
|
118
|
+
{ label: "merge — merge commits", value: "merge" }
|
|
119
|
+
]
|
|
120
|
+
prompt_choice( options: options, default: 0 )
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def prompt_choice( options:, default: )
|
|
124
|
+
options.each_with_index do |option, index|
|
|
125
|
+
puts_line " #{index + 1}) #{option.fetch( :label )}"
|
|
126
|
+
end
|
|
127
|
+
out.print "#{BADGE} Choice [#{default + 1}]: "
|
|
128
|
+
out.flush
|
|
129
|
+
raw = self.in.gets
|
|
130
|
+
return options[ default ].fetch( :value ) if raw.nil?
|
|
131
|
+
|
|
132
|
+
input = raw.to_s.strip
|
|
133
|
+
return options[ default ].fetch( :value ) if input.empty?
|
|
134
|
+
|
|
135
|
+
index = Integer( input ) - 1
|
|
136
|
+
return options[ default ].fetch( :value ) if index < 0 || index >= options.length
|
|
137
|
+
|
|
138
|
+
options[ index ].fetch( :value )
|
|
139
|
+
rescue ArgumentError
|
|
140
|
+
options[ default ].fetch( :value )
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def prompt_custom_value( label: )
|
|
144
|
+
out.print "#{BADGE} #{label}: "
|
|
145
|
+
out.flush
|
|
146
|
+
raw = self.in.gets
|
|
147
|
+
return nil if raw.nil?
|
|
148
|
+
|
|
149
|
+
value = raw.to_s.strip
|
|
150
|
+
value.empty? ? nil : value
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def build_remote_options( remotes: )
|
|
154
|
+
sorted = sort_remotes( remotes: remotes )
|
|
155
|
+
sorted.map do |entry|
|
|
156
|
+
name = entry.fetch( :name )
|
|
157
|
+
url = entry.fetch( :url )
|
|
158
|
+
{ label: "#{name} (#{url})", value: name }
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def sort_remotes( remotes: )
|
|
163
|
+
well_known = []
|
|
164
|
+
others = []
|
|
165
|
+
remotes.each do |entry|
|
|
166
|
+
if WELL_KNOWN_REMOTES.include?( entry.fetch( :name ) )
|
|
167
|
+
well_known << entry
|
|
168
|
+
else
|
|
169
|
+
others << entry
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
well_known.sort_by { |e| WELL_KNOWN_REMOTES.index( e.fetch( :name ) ) || 999 } + others.sort_by { |e| e.fetch( :name ) }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def build_main_branch_options
|
|
176
|
+
options = []
|
|
177
|
+
main_exists = branch_exists_locally_or_remote?( branch: "main" )
|
|
178
|
+
master_exists = branch_exists_locally_or_remote?( branch: "master" )
|
|
179
|
+
|
|
180
|
+
if main_exists
|
|
181
|
+
options << { label: "main", value: "main" }
|
|
182
|
+
options << { label: "master", value: "master" } if master_exists
|
|
183
|
+
elsif master_exists
|
|
184
|
+
options << { label: "master", value: "master" }
|
|
185
|
+
options << { label: "main", value: "main" }
|
|
186
|
+
else
|
|
187
|
+
options << { label: "main", value: "main" }
|
|
188
|
+
options << { label: "master", value: "master" }
|
|
189
|
+
end
|
|
190
|
+
options
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def branch_exists_locally_or_remote?( branch: )
|
|
194
|
+
return true if branch_exists?( branch_name: branch )
|
|
195
|
+
|
|
196
|
+
remote = config.git_remote
|
|
197
|
+
_, _, success, = git_run( "rev-parse", "--verify", "#{remote}/#{branch}" )
|
|
198
|
+
success
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def list_git_remotes
|
|
202
|
+
stdout_text, _, success, = git_run( "remote", "-v" )
|
|
203
|
+
return [] unless success
|
|
204
|
+
|
|
205
|
+
remotes = {}
|
|
206
|
+
stdout_text.lines.each do |line|
|
|
207
|
+
parts = line.strip.split( /\s+/ )
|
|
208
|
+
next if parts.length < 2
|
|
209
|
+
|
|
210
|
+
name = parts[ 0 ]
|
|
211
|
+
url = parts[ 1 ]
|
|
212
|
+
remotes[ name ] ||= url
|
|
213
|
+
end
|
|
214
|
+
remotes.map { |name, url| { name: name, url: url } }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def detect_git_remote
|
|
218
|
+
remotes = list_git_remotes
|
|
219
|
+
remote_names = remotes.map { |entry| entry.fetch( :name ) }
|
|
220
|
+
return nil if remote_names.empty?
|
|
221
|
+
|
|
222
|
+
return config.git_remote if remote_names.include?( config.git_remote )
|
|
223
|
+
return remote_names.first if remote_names.length == 1
|
|
224
|
+
|
|
225
|
+
candidate = WELL_KNOWN_REMOTES.find { |name| remote_names.include?( name ) }
|
|
226
|
+
return candidate unless candidate.nil?
|
|
227
|
+
|
|
228
|
+
remote_names.first
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def detect_main_branch
|
|
232
|
+
return "main" if branch_exists_locally_or_remote?( branch: "main" )
|
|
233
|
+
return "master" if branch_exists_locally_or_remote?( branch: "master" )
|
|
234
|
+
|
|
235
|
+
nil
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def write_setup_config( choices: )
|
|
239
|
+
config_data = build_config_data_from_choices( choices: choices )
|
|
240
|
+
|
|
241
|
+
config_path = Config.global_config_path( repo_root: repo_root )
|
|
242
|
+
if config_path.empty?
|
|
243
|
+
puts_line "WARN: unable to determine config path; skipping config write."
|
|
244
|
+
return EXIT_OK
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
existing_data = load_existing_config( path: config_path )
|
|
248
|
+
merged = Config.deep_merge( base: existing_data, overlay: config_data )
|
|
249
|
+
|
|
250
|
+
FileUtils.mkdir_p( File.dirname( config_path ) )
|
|
251
|
+
File.write( config_path, JSON.pretty_generate( merged ) )
|
|
252
|
+
puts_line ""
|
|
253
|
+
puts_line "Config saved to #{config_path}"
|
|
254
|
+
|
|
255
|
+
reload_config_after_setup!
|
|
256
|
+
EXIT_OK
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def build_config_data_from_choices( choices: )
|
|
260
|
+
data = {}
|
|
261
|
+
choices.each do |key, value|
|
|
262
|
+
next if value.nil?
|
|
263
|
+
|
|
264
|
+
parts = key.split( "." )
|
|
265
|
+
current = data
|
|
266
|
+
parts[ 0..-2 ].each do |part|
|
|
267
|
+
current[ part ] ||= {}
|
|
268
|
+
current = current[ part ]
|
|
269
|
+
end
|
|
270
|
+
current[ parts.last ] = value
|
|
271
|
+
end
|
|
272
|
+
data
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def load_existing_config( path: )
|
|
276
|
+
return {} unless File.file?( path )
|
|
277
|
+
|
|
278
|
+
JSON.parse( File.read( path ) )
|
|
279
|
+
rescue JSON::ParserError
|
|
280
|
+
{}
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def reload_config_after_setup!
|
|
284
|
+
@config = Config.load( repo_root: repo_root )
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def global_config_exists?
|
|
288
|
+
path = Config.global_config_path( repo_root: repo_root )
|
|
289
|
+
!path.empty? && File.file?( path )
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
include Setup
|
|
294
|
+
end
|
|
295
|
+
end
|
data/lib/carson/runtime.rb
CHANGED
|
@@ -22,11 +22,12 @@ module Carson
|
|
|
22
22
|
DISPOSITION_TOKENS = %w[accepted rejected deferred].freeze
|
|
23
23
|
|
|
24
24
|
# Runtime wiring for repository context, tool paths, and output streams.
|
|
25
|
-
def initialize( repo_root:, tool_root:, out:, err: )
|
|
25
|
+
def initialize( repo_root:, tool_root:, out:, err:, in_stream: $stdin )
|
|
26
26
|
@repo_root = repo_root
|
|
27
27
|
@tool_root = tool_root
|
|
28
28
|
@out = out
|
|
29
29
|
@err = err
|
|
30
|
+
@in = in_stream
|
|
30
31
|
@config = Config.load( repo_root: repo_root )
|
|
31
32
|
@git_adapter = Adapters::Git.new( repo_root: repo_root )
|
|
32
33
|
@github_adapter = Adapters::GitHub.new( repo_root: repo_root )
|
|
@@ -34,7 +35,7 @@ module Carson
|
|
|
34
35
|
|
|
35
36
|
private
|
|
36
37
|
|
|
37
|
-
attr_reader :repo_root, :tool_root, :out, :err, :config, :git_adapter, :github_adapter
|
|
38
|
+
attr_reader :repo_root, :tool_root, :out, :err, :in, :config, :git_adapter, :github_adapter
|
|
38
39
|
|
|
39
40
|
# Current local branch name.
|
|
40
41
|
def current_branch
|
|
@@ -187,3 +188,4 @@ require_relative "runtime/lint"
|
|
|
187
188
|
require_relative "runtime/audit"
|
|
188
189
|
require_relative "runtime/review"
|
|
189
190
|
require_relative "runtime/govern"
|
|
191
|
+
require_relative "runtime/setup"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: carson
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hailei Wang
|
|
@@ -56,6 +56,7 @@ files:
|
|
|
56
56
|
- lib/carson/runtime/review/query_text.rb
|
|
57
57
|
- lib/carson/runtime/review/sweep_support.rb
|
|
58
58
|
- lib/carson/runtime/review/utility.rb
|
|
59
|
+
- lib/carson/runtime/setup.rb
|
|
59
60
|
- lib/carson/version.rb
|
|
60
61
|
- templates/.github/AGENTS.md
|
|
61
62
|
- templates/.github/CLAUDE.md
|
|
@@ -70,6 +71,16 @@ metadata:
|
|
|
70
71
|
changelog_uri: https://github.com/wanghailei/carson/blob/main/RELEASE.md
|
|
71
72
|
bug_tracker_uri: https://github.com/wanghailei/carson/issues
|
|
72
73
|
documentation_uri: https://github.com/wanghailei/carson/blob/main/MANUAL.md
|
|
74
|
+
post_install_message: |2+
|
|
75
|
+
|
|
76
|
+
⧓ Carson at your service.
|
|
77
|
+
|
|
78
|
+
Step into your project directory and run:
|
|
79
|
+
|
|
80
|
+
carson onboard
|
|
81
|
+
|
|
82
|
+
I'll walk you through everything from there.
|
|
83
|
+
|
|
73
84
|
rdoc_options: []
|
|
74
85
|
require_paths:
|
|
75
86
|
- lib
|
|
@@ -88,3 +99,4 @@ rubygems_version: 4.0.3
|
|
|
88
99
|
specification_version: 4
|
|
89
100
|
summary: Outsider governance runtime for repository hygiene and merge readiness.
|
|
90
101
|
test_files: []
|
|
102
|
+
...
|