carson 2.11.2 → 2.12.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2ee87fb6195405a8984a69edfe853c54e4148e097ddeb68b664f3b174f76aa58
4
- data.tar.gz: 1aff6cd40925b1e29a5317062963874428759a76932b5bc87ff49b1f181a433c
3
+ metadata.gz: 87aef62f0df0f4a298b22d5fd2dcc033b3fb8ff382e7c43fcc37399fb9e28b17
4
+ data.tar.gz: 50120a7c2252236e86736fbab59eeade47b80488c89b2901849b16dc77ed2c7f
5
5
  SHA512:
6
- metadata.gz: 5431016b0e297bd23d69a781b633e6e786df4ebe5bb9625a9ed4052e8cdfb399e59e253b007d5403944f52ffdc6882c35eb0cf3babcb487740eadb178306d933
7
- data.tar.gz: 713018f54df585927a2a86dc9d922bf750214603fb3daf89991c5eec9152a85ceeaad1ea130597f2fd357f8d95be282953c12c9a408bb169fd445ce26e4c6e10
6
+ metadata.gz: 790d1b70d2df69440acc5c7d13ed7566b243e3b69a7f6d5735bc6713c275702e72273cd13ff6c958e3635669ea2411af6289eefc69e6782d3182a7dd390e1f61
7
+ data.tar.gz: e9367dd0605946d9a2f7202c0f41cb2ebebfe6e19b463e606bb88e4e1d719b3c5fbdfc76911bf8b7be1a225f5dbbdd4518b88d5f9f0a5a92ad9a841d11f1f92d
data/API.md CHANGED
@@ -16,7 +16,7 @@ carson <command> [subcommand] [arguments]
16
16
  | Command | Purpose |
17
17
  |---|---|
18
18
  | `carson setup` | Interactive quiz to configure remote, main branch, workflow, and merge method. Writes `~/.carson/config.json`. |
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 lint policy --source <path-or-git-url> [--ref <git-ref>] [--force]` | Distribute lint configs from a central source into the governed repo's `.github/linters/`. |
20
20
  | `carson onboard [repo_path]` | Apply one-command baseline setup for a target git repository. Auto-triggers `setup` on first run. |
21
21
  | `carson prepare` | Install or refresh Carson-managed global hooks. |
22
22
  | `carson refresh [repo_path]` | Re-apply hooks, templates, and audit after upgrading Carson. |
@@ -78,6 +78,7 @@ Allowed Carson-managed persistence in host repositories:
78
78
  - `.github/CLAUDE.md` — agent discovery pointer for Claude Code
79
79
  - `.github/AGENTS.md` — agent discovery pointer for Codex
80
80
  - `.github/pull_request_template.md` — PR template
81
+ - `.github/workflows/carson-lint.yml` — MegaLinter CI workflow
81
82
 
82
83
  ## Configuration interface
83
84
 
@@ -97,33 +98,31 @@ Environment overrides:
97
98
  - `CARSON_REVIEW_SWEEP_STATES`
98
99
  - `CARSON_WORKFLOW_STYLE`
99
100
  - `CARSON_RUBY_INDENTATION`
101
+ - `CARSON_LINT_COMMAND`
102
+ - `CARSON_LINT_ENFORCEMENT`
103
+ - `CARSON_LINT_POLICY_SOURCE`
100
104
 
101
- `lint.languages` schema:
105
+ `lint` schema:
102
106
 
103
107
  ```json
104
108
  {
105
109
  "lint": {
106
- "languages": {
107
- "ruby": {
108
- "enabled": true,
109
- "globs": ["**/*.rb"],
110
- "command": ["ruby", "/absolute/path/to/carson/lib/carson/policy/ruby/lint.rb", "{files}"],
111
- "config_files": ["~/.carson/lint/rubocop.yml"]
112
- }
113
- }
110
+ "command": "make lint",
111
+ "enforcement": "strict",
112
+ "policy_source": "wanghailei/lint.git"
114
113
  }
115
114
  }
116
115
  ```
117
116
 
118
- `lint.languages` semantics:
119
- - `enabled`: boolean toggle per language.
120
- - `globs`: file-match patterns applied to the selected audit target set.
121
- - `command`: argv array executed without shell interpolation.
122
- - `config_files`: required files that must exist before lint runs.
123
- - `{files}` token: replaced with matched files; if omitted, matched files are appended at the end of argv.
124
- - Default Ruby policy source is `~/.carson/lint/rubocop.yml`; Ruby execution logic is Carson-owned.
125
- - Client repositories containing repo-local `.rubocop.yml` are hard-blocked by `carson audit` in outsider mode.
126
- - Non-Ruby language entries (`javascript`, `css`, `html`, `erb`) are present but disabled by default.
117
+ `lint` semantics:
118
+ - `command`: string or array — local lint command executed during `carson audit`. If `null` (default), local lint is skipped.
119
+ - `enforcement`: `"strict"` (default) blocks on lint failure; `"advisory"` warns but does not block.
120
+ - `policy_source`: default source for `carson lint policy` when `--source` is not specified.
121
+
122
+ Environment overrides:
123
+ - `CARSON_LINT_COMMAND` overrides `lint.command`.
124
+ - `CARSON_LINT_ENFORCEMENT` overrides `lint.enforcement`.
125
+ - `CARSON_LINT_POLICY_SOURCE` overrides `lint.policy_source`.
127
126
 
128
127
  Lint target file source precedence in `carson audit`:
129
128
  - staged files for local commit-time execution.
@@ -131,14 +130,12 @@ Lint target file source precedence in `carson audit`:
131
130
  - full repository tracked files in GitHub non-PR events.
132
131
  - local working-tree changes as fallback.
133
132
 
134
- Private-source clone token for `carson lint setup`:
133
+ Private-source clone token for `carson lint policy`:
135
134
  - `CARSON_READ_TOKEN` (used when `--source` points to a private GitHub repository).
136
135
 
137
- Ruby source requirement for `carson lint setup` (when Ruby lint is enabled):
138
- - `CODING/rubocop.yml` must exist in the source tree.
139
-
140
136
  Policy layout requirement:
141
- - Language policy files are stored directly under `CODING/` and copied to `~/.carson/lint/` without language subdirectories.
137
+ - Lint config files sit at the root of the source repo and are copied to `<governed-repo>/.github/linters/`.
138
+ - MegaLinter auto-discovers configs in `.github/linters/` during CI.
142
139
 
143
140
  ## Output interface
144
141
 
data/MANUAL.md CHANGED
@@ -27,22 +27,20 @@ carson version
27
27
 
28
28
  ### Step 1: Prepare your lint policy
29
29
 
30
- Carson enforces lint rules from a central policy source — a directory or git repository you control that contains a `CODING/` folder. For Ruby governance, the required file is `CODING/rubocop.yml`.
31
-
32
- Run `lint setup` to copy policy files into `~/.carson/lint/`:
30
+ Carson distributes lint configuration files from a central policy source — a directory or git repository you control into each governed repo's `.github/linters/` directory, where MegaLinter auto-discovers them.
33
31
 
34
32
  ```bash
35
- carson lint setup --source /path/to/your-policy-repo
33
+ carson lint policy --source /path/to/your-policy-repo
36
34
  ```
37
35
 
38
- After this command, `~/.carson/lint/rubocop.yml` exists and is ready for Carson to use. Every governed repository will reference these same policy files — this is how Carson keeps lint consistent.
36
+ After this command, `.github/linters/` contains your lint configs (`.rubocop.yml`, `biome.json`, `ruff.toml`, etc.). MegaLinter uses these in CI. Every governed repository gets the same rules — this is how Carson keeps lint consistent across languages.
39
37
 
40
38
  Options:
41
39
  - `--source <path-or-git-url>` — where to read policy files from (required).
42
40
  - `--ref <git-ref>` — branch or tag when `--source` is a git URL.
43
- - `--force` — overwrite existing `~/.carson/lint` files.
41
+ - `--force` — overwrite existing `.github/linters/` files.
44
42
 
45
- Policy layout: language config files sit directly under `CODING/` (flat layout, no language subfolders). Non-Ruby entries are present but disabled by default.
43
+ Policy layout: lint config files sit at the root of the source repo (flat layout, no subdirectories required).
46
44
 
47
45
  ### Step 2: Onboard a repository
48
46
 
@@ -105,7 +103,7 @@ Notes:
105
103
 
106
104
  ```bash
107
105
  carson sync # fast-forward local main
108
- carson lint setup --source /path/to/your-policy-repo # refresh policy if needed
106
+ carson lint policy --source /path/to/your-policy-repo # refresh policy if needed
109
107
  carson audit # full governance check
110
108
  ```
111
109
 
@@ -183,7 +181,7 @@ Carson's `govern.merge.method` controls how `carson govern` merges ready PRs. Th
183
181
  These define what Carson *is*. They are not configurable.
184
182
 
185
183
  - **Outsider boundary** — Carson never places its own artefacts inside a governed repository.
186
- - **Centralised lint** — one policy source at `~/.carson/lint/`, shared across all repos, zero per-repo drift.
184
+ - **Centralised lint** — one policy source distributed into each repo's `.github/linters/`, zero per-repo drift.
187
185
  - **Active review** — undisposed reviewer findings block merge; feedback must be acknowledged.
188
186
  - **Self-diagnosing output** — every message names the cause and the fix.
189
187
  - **Transparent governance** — Carson prepares everything for merge but never makes decisions without telling you.
@@ -238,11 +236,12 @@ Change: `CARSON_HOOKS_BASE_PATH`.
238
236
 
239
237
  #### Lint policy source
240
238
 
241
- Where lint configuration files live.
239
+ Where lint configuration files come from and where they land.
242
240
 
243
- - Default: **`~/.carson/lint/`**. Centralised: one policy source governs all repos consistently. Repo-local `.rubocop.yml` is forbidden in outsider mode to prevent per-repo drift.
241
+ - Default source: **`wanghailei/lint.git`**. A central repository containing lint configs for all languages.
242
+ - Target: **`<repo>/.github/linters/`**. MegaLinter auto-discovers configs here in CI.
244
243
 
245
- Change the source: `carson lint setup --source <path-or-git-url>`.
244
+ Change: `carson lint policy --source <path-or-git-url>` or `lint.policy_source` in config.
246
245
 
247
246
  #### Scope integrity
248
247
 
@@ -314,7 +313,7 @@ Common environment overrides:
314
313
  | `CARSON_WORKFLOW_STYLE` | Workflow style override (`branch` or `trunk`). |
315
314
  | `CARSON_RUBY_INDENTATION` | Ruby indentation policy (`tabs`, `spaces`, or `either`). |
316
315
 
317
- For the full configuration schema and `lint.languages` definition, see `API.md`.
316
+ For the full configuration schema, see `API.md`.
318
317
 
319
318
  ## Troubleshooting
320
319
 
@@ -333,8 +332,8 @@ carson template apply
333
332
  carson template check
334
333
  ```
335
334
 
336
- **Audit blocks on repo-local `.rubocop.yml`**
337
- - Carson hard-blocks governed repositories that contain their own `.rubocop.yml`. Remove the repo-local file and rely on the central policy in `~/.carson/lint/rubocop.yml`.
335
+ **Audit blocks on lint failure**
336
+ - If `lint.command` is configured and fails, audit blocks (in `strict` mode) or warns (in `advisory` mode). Fix the lint issues and re-run `carson audit`.
338
337
 
339
338
  **Hook version mismatch after upgrade**
340
339
  - Run `carson refresh` to re-apply hooks and templates for the new Carson version.
data/README.md CHANGED
@@ -22,7 +22,6 @@ Carson is an autonomous governance runtime that lives on your workstation and in
22
22
  │ │
23
23
  │ ~/.carson/ Carson config │
24
24
  │ ~/.carson/hooks/ Git hooks │
25
- │ ~/.carson/lint/ Lint policy │
26
25
  │ ~/.carson/cache/ Reports │
27
26
  │ ~/.carson/govern/ Dispatch state │
28
27
  │ │
@@ -45,7 +44,7 @@ This separation is Carson's defining trait — the **outsider boundary**: no Car
45
44
  Carson is opinionated about governance. These are non-negotiable principles, not configurable defaults:
46
45
 
47
46
  - **Outsider boundary** — Carson lives outside your repo, never inside. No Carson-owned artefacts in your repository. Offboarding leaves no trace.
48
- - **Centralised lint** — lint policy at `~/.carson/lint/`, shared across all repos. Repo-local config files are forbidden — one source of truth, zero drift.
47
+ - **Centralised lint** — lint policy distributed from a central source into each repo's `.github/linters/`. One source of truth, zero drift.
49
48
  - **Active review** — undisposed reviewer findings block merge. Feedback must be acknowledged, not buried.
50
49
  - **Self-diagnosing output** — every message names the cause and the fix. If you need to debug Carson's output, the output failed.
51
50
  - **Transparent governance** — Carson prepares everything for merge but never oversteps. It does not make decisions for you without telling you.
@@ -54,8 +53,8 @@ Everything else — workflow style, merge method, remote name, main branch — i
54
53
 
55
54
  The data flow:
56
55
 
57
- 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`.
58
- 2. `carson onboard` installs git hooks, synchronises `.github/*` templates, and runs a first governance audit on a host repository.
56
+ 1. You maintain a **policy source** — a directory or git repository containing your lint config files (e.g. `.rubocop.yml`, `biome.json`, `ruff.toml`). `carson lint policy --source <repo>` copies these into each governed repo's `.github/linters/`, where MegaLinter auto-discovers them.
57
+ 2. `carson onboard` installs git hooks, synchronises `.github/*` templates (including a MegaLinter CI workflow), and runs a first governance audit on a host repository.
59
58
  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.
60
59
  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**.
61
60
  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.
@@ -75,7 +74,7 @@ The data flow:
75
74
 
76
75
  | Command | What it does |
77
76
  |---|---|
78
- | `carson lint setup` | Seed `~/.carson/lint/` from your policy source. |
77
+ | `carson lint policy --source <repo>` | Distribute lint configs from policy source into `.github/linters/`. |
79
78
  | `carson onboard` | One-command baseline: hooks + templates + first audit. |
80
79
  | `carson prepare` | Install or refresh Carson-managed global hooks. |
81
80
  | `carson refresh` | Re-apply hooks, templates, and audit after upgrading Carson. |
@@ -116,10 +115,10 @@ gem install --user-install carson
116
115
  carson version
117
116
  ```
118
117
 
119
- **Prepare your lint policy.** A policy source is any directory (or git URL) that contains a `CODING/` folder with your lint configuration files. For Ruby, the required file is `CODING/rubocop.yml`. Carson copies these into `~/.carson/lint/` so that every governed repository uses the same rules:
118
+ **Prepare your lint policy.** A policy source is any directory (or git URL) containing your lint configuration files (`.rubocop.yml`, `biome.json`, `ruff.toml`, etc.). Carson copies these into the governed repo's `.github/linters/` where MegaLinter auto-discovers them:
120
119
 
121
120
  ```bash
122
- carson lint setup --source /path/to/your-policy-repo
121
+ carson lint policy --source /path/to/your-policy-repo
123
122
  ```
124
123
 
125
124
  **Onboard a repository:**
data/RELEASE.md CHANGED
@@ -5,6 +5,44 @@ 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.12.0 — Language-Agnostic Lint Policy Distribution + MegaLinter
9
+
10
+ ### What changed
11
+
12
+ - **Lint policy distribution is now language-agnostic.** `carson lint policy --source <path-or-git-url>` copies all files from the source repo root into the governed repo's `.github/linters/` directory, where MegaLinter auto-discovers them. Works for any linter config: rubocop.yml, biome.json, ruff.toml, .erb-lint.yml, etc.
13
+ - **New `lint.command` config key.** Local audit lint is now a single user-configured command (e.g. `"make lint"`, `"trunk check"`, `["ruff", "check"]`). Replaces the old per-language `lint.languages` system entirely.
14
+ - **New `lint.enforcement` config key.** `"strict"` (default) blocks on lint failure; `"advisory"` warns but does not block.
15
+ - **New `lint.policy_source` config key.** Default: `wanghailei/lint.git`. Sets the default source for lint policy distribution.
16
+ - **MegaLinter CI workflow template.** `carson onboard` now installs `.github/workflows/carson-lint.yml`, which runs MegaLinter on PRs and pushes to main.
17
+ - **Interactive setup prompts.** `carson setup` now asks for lint command and enforcement mode.
18
+ - **Removed `lint.languages`** — all per-language lint configuration, Ruby-specific lint runners, and hardcoded language definitions are gone.
19
+ - **Removed `lint setup` subcommand alias** — use `carson lint policy` instead.
20
+ - **Removed `--legacy` flag** from `carson lint policy`.
21
+ - **Source repo layout simplified** — lint config files live at the source repo root; no `CODING/` subdirectory required.
22
+
23
+ ### Breaking changes
24
+
25
+ - `lint.languages` config key no longer exists. If your config references it, remove it.
26
+ - `carson lint setup` no longer works. Use `carson lint policy --source <path-or-git-url>`.
27
+ - Lint policy files are now written to `<repo>/.github/linters/` (not `~/.carson/lint/`).
28
+
29
+ ### What users must do now
30
+
31
+ 1. Upgrade Carson to `2.12.0` and run `carson refresh`.
32
+ 2. Remove any `lint.languages` entries from your Carson config.
33
+ 3. Set `lint.command` in your config if you want local lint during audit (e.g. `"make lint"`).
34
+ 4. Run `carson lint policy --source <your-policy-repo>` to distribute linter configs to `.github/linters/`.
35
+
36
+ ## 2.11.3 — Refine RubyGems Description Tone
37
+
38
+ ### What changed
39
+
40
+ - **Gemspec** — rewrote summary and description with engineer-professional tone. Factual, concrete, no slogans.
41
+
42
+ ### What users must do now
43
+
44
+ Nothing. Metadata only.
45
+
8
46
  ## 2.11.2 — Improve RubyGems Summary and Description
9
47
 
10
48
  ### What changed
data/SKILL.md CHANGED
@@ -33,7 +33,7 @@ When you see exit 2, do NOT bypass it. Read the output, fix the root cause, and
33
33
  Carson audit output is structured as labelled key-value lines prefixed with ⧓. Key sections:
34
34
 
35
35
  - **Working Tree** — staged/unstaged status.
36
- - **Local Lint Quality** — per-language lint results. `lint_ruby_status: ok` means clean.
36
+ - **Local Lint Quality** — lint command result. `lint_command_status: ok` means clean.
37
37
  - **Main Sync Status** — whether local main matches remote. If ahead, reset drift before committing.
38
38
  - **Scope Integrity Guard** — checks that commits stay within a single business intent and scope group.
39
39
  - **Audit Result** — final verdict: `status: ok` (clean), `status: attention` (advisory, not blocking), `status: block` (must fix).
@@ -99,4 +99,4 @@ Check that `govern.merge.method` in config matches what GitHub allows. If the re
99
99
  - Carson never lives inside governed repositories. No `.carson.yml`, no `bin/carson`, no `.tools/carson/`.
100
100
  - Carson-managed files in repos are limited to `.github/*` templates.
101
101
  - Carson's hooks live at `~/.carson/hooks/<version>/`, never in `.git/hooks/`.
102
- - Lint policy lives at `~/.carson/lint/`, seeded by `carson lint setup --source <policy-repo>`.
102
+ - Lint policy is distributed via `carson lint policy --source <policy-repo>` into each repo's `.github/linters/`.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.11.2
1
+ 2.12.0
data/carson.gemspec CHANGED
@@ -7,8 +7,8 @@ Gem::Specification.new do |spec|
7
7
  spec.version = Carson::VERSION
8
8
  spec.authors = [ "Hailei Wang" ]
9
9
  spec.email = [ "wanghailei@users.noreply.github.com" ]
10
- spec.summary = "You write the code, Carson manages everything from commit to merge."
11
- spec.description = "Carson is an autonomous governance runtime that lives outside your repositories. It enforces lint policy on every commit, gates merges on unresolved reviewer feedback, triages open PRs across your entire portfolio, dispatches coding agents to fix failures, merges what's ready, and cleans up after itself. One gem, all your projects, unmanned."
10
+ spec.summary = "Autonomous governance runtime — lint, review gates, PR triage, and merge across repositories."
11
+ spec.description = "Carson lives outside the repositories it governs. On every commit it enforces centralised lint policy and scope checks. On PRs it gates merge on unresolved reviewer feedback, dispatches coding agents to fix CI failures, merges passing PRs, and housekeeps branches. Runs locally and in GitHub Actions."
12
12
  spec.homepage = "https://github.com/wanghailei/carson"
13
13
  spec.license = "MIT"
14
14
  spec.required_ruby_version = ">= 3.4"
data/lib/carson/cli.rb CHANGED
@@ -47,7 +47,7 @@ module Carson
47
47
 
48
48
  def self.build_parser
49
49
  OptionParser.new do |opts|
50
- 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]"
50
+ opts.banner = "Usage: carson [setup|audit|sync|prune|prepare|inspect|onboard [repo_path]|refresh [repo_path]|offboard [repo_path]|template check|template apply|lint policy --source <path-or-git-url>|review gate|review sweep|govern [--dry-run] [--json] [--loop SECONDS]|housekeep|version]"
51
51
  end
52
52
  end
53
53
 
@@ -112,8 +112,8 @@ module Carson
112
112
 
113
113
  def self.parse_lint_subcommand( argv:, parser:, err: )
114
114
  action = argv.shift
115
- unless action == "setup"
116
- err.puts "#{BADGE} Missing or invalid subcommand for lint. Use: carson lint setup --source <path-or-git-url> [--ref <git-ref>] [--force]"
115
+ unless action == "policy"
116
+ err.puts "#{BADGE} Missing or invalid subcommand for lint. Use: carson lint policy --source <path-or-git-url> [--ref <git-ref>] [--force]"
117
117
  err.puts parser
118
118
  return { command: :invalid }
119
119
  end
@@ -124,19 +124,19 @@ module Carson
124
124
  force: false
125
125
  }
126
126
  lint_parser = OptionParser.new do |opts|
127
- opts.banner = "Usage: carson lint setup --source <path-or-git-url> [--ref <git-ref>] [--force]"
127
+ opts.banner = "Usage: carson lint policy --source <path-or-git-url> [--ref <git-ref>] [--force]"
128
128
  opts.on( "--source SOURCE", "Source repository path or git URL that contains CODING/" ) { |value| options[ :source ] = value.to_s.strip }
129
129
  opts.on( "--ref REF", "Git ref used when --source is a git URL (default: main)" ) { |value| options[ :ref ] = value.to_s.strip }
130
- opts.on( "--force", "Overwrite existing files in ~/.carson/lint" ) { options[ :force ] = true }
130
+ opts.on( "--force", "Overwrite existing files" ) { options[ :force ] = true }
131
131
  end
132
132
  lint_parser.parse!( argv )
133
133
  if options.fetch( :source ).to_s.empty?
134
- err.puts "#{BADGE} Missing required --source for lint setup."
134
+ err.puts "#{BADGE} Missing required --source for lint policy."
135
135
  err.puts lint_parser
136
136
  return { command: :invalid }
137
137
  end
138
138
  unless argv.empty?
139
- err.puts "#{BADGE} Unexpected arguments for lint setup: #{argv.join( ' ' )}"
139
+ err.puts "#{BADGE} Unexpected arguments for lint policy: #{argv.join( ' ' )}"
140
140
  err.puts lint_parser
141
141
  return { command: :invalid }
142
142
  end
data/lib/carson/config.rb CHANGED
@@ -8,7 +8,8 @@ module Carson
8
8
  class Config
9
9
  attr_accessor :git_remote
10
10
  attr_reader :main_branch, :protected_branches, :hooks_base_path, :required_hooks,
11
- :path_groups, :template_managed_files, :lint_languages,
11
+ :path_groups, :template_managed_files,
12
+ :lint_command, :lint_enforcement, :lint_policy_source,
12
13
  :review_wait_seconds, :review_poll_seconds, :review_max_polls, :review_sweep_window_days,
13
14
  :review_sweep_states, :review_disposition_prefix, :review_risk_keywords,
14
15
  :review_tracking_issue_title, :review_tracking_issue_label, :review_bot_usernames,
@@ -47,10 +48,12 @@ module Carson
47
48
  }
48
49
  },
49
50
  "template" => {
50
- "managed_files" => [ ".github/carson-instructions.md", ".github/copilot-instructions.md", ".github/CLAUDE.md", ".github/AGENTS.md", ".github/pull_request_template.md" ]
51
+ "managed_files" => [ ".github/carson-instructions.md", ".github/copilot-instructions.md", ".github/CLAUDE.md", ".github/AGENTS.md", ".github/pull_request_template.md", ".github/workflows/carson-lint.yml" ]
51
52
  },
52
53
  "lint" => {
53
- "languages" => default_lint_languages_data
54
+ "command" => nil,
55
+ "enforcement" => "strict",
56
+ "policy_source" => "wanghailei/lint.git"
54
57
  },
55
58
  "workflow" => {
56
59
  "style" => "branch"
@@ -94,42 +97,6 @@ module Carson
94
97
  }
95
98
  end
96
99
 
97
- def self.default_lint_languages_data
98
- ruby_runner = File.expand_path( "policy/ruby/lint.rb", __dir__ )
99
- {
100
- "ruby" => {
101
- "enabled" => true,
102
- "globs" => [ "**/*.rb", "Gemfile", "*.gemspec", "Rakefile" ],
103
- "command" => [ "ruby", ruby_runner, "{files}" ],
104
- "config_files" => [ "~/.carson/lint/rubocop.yml" ]
105
- },
106
- "javascript" => {
107
- "enabled" => false,
108
- "globs" => [ "**/*.js", "**/*.mjs", "**/*.cjs", "**/*.jsx" ],
109
- "command" => [ "node", "~/.carson/lint/javascript.lint.js", "{files}" ],
110
- "config_files" => [ "~/.carson/lint/javascript.lint.js" ]
111
- },
112
- "css" => {
113
- "enabled" => false,
114
- "globs" => [ "**/*.css" ],
115
- "command" => [ "node", "~/.carson/lint/css.lint.js", "{files}" ],
116
- "config_files" => [ "~/.carson/lint/css.lint.js" ]
117
- },
118
- "html" => {
119
- "enabled" => false,
120
- "globs" => [ "**/*.html" ],
121
- "command" => [ "node", "~/.carson/lint/html.lint.js", "{files}" ],
122
- "config_files" => [ "~/.carson/lint/html.lint.js" ]
123
- },
124
- "erb" => {
125
- "enabled" => false,
126
- "globs" => [ "**/*.erb" ],
127
- "command" => [ "ruby", "~/.carson/lint/erb.lint.rb", "{files}" ],
128
- "config_files" => [ "~/.carson/lint/erb.lint.rb" ]
129
- }
130
- }
131
- end
132
-
133
100
  def self.load_global_config_data( repo_root: )
134
101
  path = global_config_path( repo_root: repo_root )
135
102
  return {} if path.empty? || !File.file?( path )
@@ -200,6 +167,13 @@ module Carson
200
167
  audit = fetch_hash_section( data: copy, key: "audit" )
201
168
  advisory_names = env_string_array( key: "CARSON_AUDIT_ADVISORY_CHECK_NAMES" )
202
169
  audit[ "advisory_check_names" ] = advisory_names unless advisory_names.empty?
170
+ lint = fetch_hash_section( data: copy, key: "lint" )
171
+ lint_command_env = ENV.fetch( "CARSON_LINT_COMMAND", "" ).to_s.strip
172
+ lint[ "command" ] = lint_command_env unless lint_command_env.empty?
173
+ lint_enforcement_env = ENV.fetch( "CARSON_LINT_ENFORCEMENT", "" ).to_s.strip
174
+ lint[ "enforcement" ] = lint_enforcement_env unless lint_enforcement_env.empty?
175
+ lint_policy_source_env = ENV.fetch( "CARSON_LINT_POLICY_SOURCE", "" ).to_s.strip
176
+ lint[ "policy_source" ] = lint_policy_source_env unless lint_policy_source_env.empty?
203
177
  style = fetch_hash_section( data: copy, key: "style" )
204
178
  ruby_indentation = ENV.fetch( "CARSON_RUBY_INDENTATION", "" ).to_s.strip
205
179
  style[ "ruby_indentation" ] = ruby_indentation unless ruby_indentation.empty?
@@ -248,9 +222,10 @@ module Carson
248
222
  @path_groups = fetch_hash( hash: fetch_hash( hash: data, key: "scope" ), key: "path_groups" ).transform_values { |value| normalize_patterns( value: value ) }
249
223
 
250
224
  @template_managed_files = fetch_string_array( hash: fetch_hash( hash: data, key: "template" ), key: "managed_files" )
251
- @lint_languages = normalize_lint_languages(
252
- languages_hash: fetch_hash( hash: fetch_hash( hash: data, key: "lint" ), key: "languages" )
253
- )
225
+ lint_hash = fetch_hash( hash: data, key: "lint" )
226
+ @lint_command = normalize_lint_command_setting( value: lint_hash[ "command" ] )
227
+ @lint_enforcement = normalize_lint_enforcement( value: lint_hash.fetch( "enforcement", "strict" ) )
228
+ @lint_policy_source = lint_hash.fetch( "policy_source", "" ).to_s.strip
254
229
 
255
230
  workflow_hash = fetch_hash( hash: data, key: "workflow" )
256
231
  @workflow_style = fetch_string( hash: workflow_hash, key: "style" ).downcase
@@ -296,7 +271,6 @@ module Carson
296
271
  raise ConfigError, "hooks.base_path cannot be empty" if hooks_base_path.empty?
297
272
  raise ConfigError, "hooks.required_hooks cannot be empty" if required_hooks.empty?
298
273
  raise ConfigError, "scope.path_groups cannot be empty" if path_groups.empty?
299
- raise ConfigError, "lint.languages cannot be empty" if lint_languages.empty?
300
274
  raise ConfigError, "review.required_disposition_prefix cannot be empty" if review_disposition_prefix.empty?
301
275
  raise ConfigError, "review.risk_keywords cannot be empty" if review_risk_keywords.empty?
302
276
  raise ConfigError, "review.sweep.states must contain one or both of open, closed" if ( review_sweep_states - [ "open", "closed" ] ).any? || review_sweep_states.empty?
@@ -364,56 +338,21 @@ module Carson
364
338
  patterns
365
339
  end
366
340
 
367
- def normalize_lint_languages( languages_hash: )
368
- raise ConfigError, "lint.languages must be an object" unless languages_hash.is_a?( Hash )
369
- normalised = {}
370
- languages_hash.each do |language_key, raw_entry|
371
- language = language_key.to_s.strip.downcase
372
- raise ConfigError, "lint.languages contains blank language key" if language.empty?
373
- raise ConfigError, "lint.languages.#{language} must be an object" unless raw_entry.is_a?( Hash )
374
-
375
- normalised[ language ] = normalize_lint_language_entry( language: language, raw_entry: raw_entry )
341
+ def normalize_lint_command_setting( value: )
342
+ return nil if value.nil?
343
+ return value.to_s.strip if value.is_a?( String )
344
+ if value.is_a?( Array )
345
+ parts = value.map { |entry| entry.to_s.strip }.reject( &:empty? )
346
+ raise ConfigError, "lint.command array must contain at least one argument" if parts.empty?
347
+ return parts
376
348
  end
377
- normalised
378
- end
379
-
380
- def normalize_lint_language_entry( language:, raw_entry: )
381
- {
382
- enabled: fetch_optional_boolean(
383
- hash: raw_entry,
384
- key: "enabled",
385
- default: true,
386
- key_path: "lint.languages.#{language}.enabled"
387
- ),
388
- globs: normalize_lint_globs( language: language, value: raw_entry[ "globs" ] ),
389
- command: normalize_lint_command( language: language, value: raw_entry[ "command" ] ),
390
- config_files: normalize_lint_config_files( language: language, value: raw_entry[ "config_files" ] )
391
- }
392
- end
393
-
394
- def normalize_lint_globs( language:, value: )
395
- raise ConfigError, "lint.languages.#{language}.globs must be an array" unless value.is_a?( Array )
396
- patterns = Array( value ).map { |entry| entry.to_s.strip }.reject( &:empty? )
397
- raise ConfigError, "lint.languages.#{language}.globs must contain at least one pattern" if patterns.empty?
398
- patterns
399
- end
400
-
401
- def normalize_lint_command( language:, value: )
402
- raise ConfigError, "lint.languages.#{language}.command must be an array" unless value.is_a?( Array )
403
- command = Array( value ).map { |entry| entry.to_s.strip }.reject( &:empty? )
404
- raise ConfigError, "lint.languages.#{language}.command must contain at least one argument" if command.empty?
405
- command
349
+ raise ConfigError, "lint.command must be a string, array, or null"
406
350
  end
407
351
 
408
- def normalize_lint_config_files( language:, value: )
409
- raise ConfigError, "lint.languages.#{language}.config_files must be an array" unless value.is_a?( Array )
410
- files = Array( value ).map { |entry| entry.to_s.strip }.reject( &:empty? )
411
- raise ConfigError, "lint.languages.#{language}.config_files must contain at least one path" if files.empty?
412
- files.map do |path|
413
- expanded = path.start_with?( "~" ) ? File.expand_path( path ) : path
414
- raise ConfigError, "lint.languages.#{language}.config_files entries must be absolute paths or ~/ paths" unless expanded.start_with?( "/" )
415
- expanded
416
- end
352
+ def normalize_lint_enforcement( value: )
353
+ text = value.to_s.strip.downcase
354
+ raise ConfigError, "lint.enforcement must be one of strict, advisory" unless [ "strict", "advisory" ].include?( text )
355
+ text
417
356
  end
418
357
 
419
358
  def fetch_optional_boolean( hash:, key:, default:, key_path: nil )
@@ -162,9 +162,43 @@ module Carson
162
162
  report
163
163
  end
164
164
 
165
- # Enforces configured multi-language lint policy before governance passes.
165
+ # Enforces configured lint policy before governance passes.
166
+ # Runs lint.command and gates on exit code. Skips when lint.command is not set.
166
167
  def local_lint_quality_report
168
+ unless config.lint_command
169
+ report = {
170
+ status: "ok",
171
+ skip_reason: "lint.command not configured",
172
+ target_source: "none",
173
+ target_files_count: 0,
174
+ blocking_languages: 0,
175
+ languages: []
176
+ }
177
+ puts_verbose "lint: SKIP (lint.command not configured)"
178
+ return report
179
+ end
180
+
181
+ lint_command_report
182
+ rescue StandardError => e
183
+ report = {
184
+ status: "block",
185
+ skip_reason: e.message,
186
+ target_source: "unknown",
187
+ target_files_count: 0,
188
+ blocking_languages: 0,
189
+ languages: []
190
+ }
191
+ puts_line "BLOCK: local lint quality check failed (#{e.message})."
192
+ report
193
+ end
194
+
195
+ # Runs the lint.command and returns a structured report.
196
+ def lint_command_report
167
197
  target_files, target_source = lint_target_files
198
+ advisory = config.lint_enforcement == "advisory"
199
+ command_value = config.lint_command
200
+ command_string = command_value.is_a?( Array ) ? command_value.join( " " ) : command_value.to_s
201
+
168
202
  report = {
169
203
  status: "ok",
170
204
  skip_reason: nil,
@@ -175,32 +209,63 @@ module Carson
175
209
  }
176
210
  puts_verbose "lint_target_source: #{target_source}"
177
211
  puts_verbose "lint_target_files_total: #{target_files.count}"
178
- config.lint_languages.each do |language, entry|
179
- language_report = lint_language_report(
180
- language: language,
181
- entry: entry,
182
- target_files: target_files
183
- )
184
- report.fetch( :languages ) << language_report
185
- next unless language_report.fetch( :status ) == "block"
212
+ puts_verbose "lint_command: #{command_string}"
213
+ puts_verbose "lint_enforcement: #{config.lint_enforcement}"
186
214
 
187
- report[ :status ] = "block"
188
- report[ :blocking_languages ] += 1
215
+ args = command_string.split( /\s+/ )
216
+ command_name = args.first.to_s.strip
217
+ unless command_available_for_lint?( command_name: command_name )
218
+ language_report = {
219
+ language: "lint.command",
220
+ enabled: true,
221
+ status: "block",
222
+ reason: "command not available: #{command_name}",
223
+ file_count: target_files.count,
224
+ files: target_files,
225
+ command: args,
226
+ config_files: [],
227
+ exit_code: EXIT_BLOCK
228
+ }
229
+ report[ :languages ] << language_report
230
+ report[ :status ] = advisory ? "ok" : "block"
231
+ report[ :blocking_languages ] = advisory ? 0 : 1
232
+ puts_verbose "lint_command_status: #{language_report.fetch( :status )}"
233
+ puts_line "WARN: lint command not available: #{command_name}" if advisory
234
+ return report
189
235
  end
190
- puts_verbose "lint_blocking_languages: #{report.fetch( :blocking_languages )}"
191
- report
192
- rescue StandardError => e
193
- report ||= {
194
- status: "block",
195
- skip_reason: nil,
196
- target_source: "unknown",
197
- target_files_count: 0,
198
- blocking_languages: 0,
199
- languages: []
236
+
237
+ stdout_text, stderr_text, success, exit_code = local_command( *args )
238
+ language_report = {
239
+ language: "lint.command",
240
+ enabled: true,
241
+ status: success ? "ok" : "block",
242
+ reason: success ? nil : summarise_command_output(
243
+ stdout_text: stdout_text,
244
+ stderr_text: stderr_text,
245
+ fallback: "lint command failed"
246
+ ),
247
+ file_count: target_files.count,
248
+ files: target_files,
249
+ command: args,
250
+ config_files: [],
251
+ exit_code: exit_code
200
252
  }
201
- report[ :status ] = "block"
202
- report[ :skip_reason ] = e.message
203
- puts_line "BLOCK: local lint quality check failed (#{e.message})."
253
+ report[ :languages ] << language_report
254
+
255
+ unless success
256
+ if advisory
257
+ report[ :status ] = "ok"
258
+ puts_verbose "lint_command_status: advisory_warn (exit #{exit_code})"
259
+ puts_line "WARN: lint command failed (exit #{exit_code}) — advisory mode, not blocking."
260
+ else
261
+ report[ :status ] = "block"
262
+ report[ :blocking_languages ] = 1
263
+ puts_verbose "lint_command_status: block (exit #{exit_code})"
264
+ end
265
+ else
266
+ puts_verbose "lint_command_status: ok"
267
+ end
268
+
204
269
  report
205
270
  end
206
271
 
@@ -274,85 +339,6 @@ module Carson
274
339
  end.compact.uniq
275
340
  end
276
341
 
277
- def lint_language_report( language:, entry:, target_files: )
278
- globs = entry.fetch( :globs )
279
- candidate_files = Array( target_files ).select do |path|
280
- globs.any? { |pattern| pattern_matches_path?( pattern: pattern, path: path ) }
281
- end
282
- report = {
283
- language: language,
284
- enabled: entry.fetch( :enabled ),
285
- status: "ok",
286
- reason: nil,
287
- file_count: candidate_files.count,
288
- files: candidate_files,
289
- command: entry.fetch( :command ),
290
- config_files: entry.fetch( :config_files ),
291
- exit_code: 0
292
- }
293
- puts_verbose "lint_language: #{language} enabled=#{report.fetch( :enabled )} files=#{report.fetch( :file_count )}"
294
- if language == "ruby" && outsider_mode?
295
- local_rubocop_path = File.join( repo_root, ".rubocop.yml" )
296
- if File.file?( local_rubocop_path )
297
- report[ :status ] = "block"
298
- report[ :reason ] = "repo-local RuboCop config is forbidden: #{relative_path( local_rubocop_path )}; remove it and use ~/.carson/lint/rubocop.yml."
299
- report[ :exit_code ] = EXIT_BLOCK
300
- puts_verbose "lint_#{language}_status: block"
301
- puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
302
- puts_verbose "ACTION: remove .rubocop.yml from this repository and run carson lint setup --source <path-or-git-url>."
303
- return report
304
- end
305
- end
306
- return report unless report.fetch( :enabled )
307
- return report if candidate_files.empty?
308
-
309
- missing_config_files = entry.fetch( :config_files ).reject { |path| File.file?( path ) }
310
- unless missing_config_files.empty?
311
- report[ :status ] = "block"
312
- report[ :reason ] = "missing config files: #{missing_config_files.join( ', ' )}"
313
- report[ :exit_code ] = EXIT_BLOCK
314
- puts_verbose "lint_#{language}_status: block"
315
- puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
316
- puts_verbose "ACTION: run carson lint setup --source <path-or-git-url> to prepare ~/.carson/lint policy files."
317
- return report
318
- end
319
-
320
- command = Array( entry.fetch( :command ) )
321
- command_name = command.first.to_s.strip
322
- if command_name.empty?
323
- report[ :status ] = "block"
324
- report[ :reason ] = "missing lint command"
325
- report[ :exit_code ] = EXIT_BLOCK
326
- puts_verbose "lint_#{language}_status: block"
327
- puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
328
- return report
329
- end
330
- unless command_available_for_lint?( command_name: command_name )
331
- report[ :status ] = "block"
332
- report[ :reason ] = "command not available: #{command_name}"
333
- report[ :exit_code ] = EXIT_BLOCK
334
- puts_verbose "lint_#{language}_status: block"
335
- puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}"
336
- return report
337
- end
338
-
339
- args = expanded_lint_command_args( command: command, files: candidate_files )
340
- stdout_text, stderr_text, success, exit_code = local_command( *args )
341
- report[ :exit_code ] = exit_code
342
- unless success
343
- report[ :status ] = "block"
344
- report[ :reason ] = summarise_command_output(
345
- stdout_text: stdout_text,
346
- stderr_text: stderr_text,
347
- fallback: "lint command failed for #{language}"
348
- )
349
- end
350
- puts_verbose "lint_#{language}_status: #{report.fetch( :status )}"
351
- puts_verbose "lint_#{language}_exit: #{report.fetch( :exit_code )}"
352
- puts_verbose "lint_#{language}_reason: #{report.fetch( :reason )}" unless report.fetch( :reason ).nil?
353
- report
354
- end
355
-
356
342
  def command_available_for_lint?( command_name: )
357
343
  return false if command_name.to_s.strip.empty?
358
344
 
@@ -373,26 +359,6 @@ module Carson
373
359
  end
374
360
  end
375
361
 
376
- def expanded_lint_command_args( command:, files: )
377
- expanded_command = Array( command ).map do |arg|
378
- text = arg.to_s
379
- if text == "{files}"
380
- text
381
- elsif text.start_with?( "~" )
382
- File.expand_path( text )
383
- elsif text.include?( "/" ) && !text.start_with?( "/" )
384
- File.expand_path( text, repo_root )
385
- else
386
- text
387
- end
388
- end
389
- if expanded_command.include?( "{files}" )
390
- return expanded_command.flat_map { |arg| arg == "{files}" ? Array( files ) : arg }
391
- end
392
-
393
- expanded_command + Array( files )
394
- end
395
-
396
362
  # Local command runner for repository-context tools used by audit lint checks.
397
363
  def local_command( *args )
398
364
  stdout_text, stderr_text, status = Open3.capture3( *args, chdir: repo_root )
@@ -5,13 +5,14 @@ require "tmpdir"
5
5
  module Carson
6
6
  class Runtime
7
7
  module Lint
8
- # Prepares canonical lint policy files under ~/.carson/lint from an explicit source.
9
- def lint_setup!( source:, ref: "main", force: false )
8
+ # Distributes lint policy files from a central source into the governed repository.
9
+ # Target: <repo>/.github/linters/ (MegaLinter auto-discovers here).
10
+ def lint_setup!( source:, ref: "main", force: false, **_ )
10
11
  puts_verbose ""
11
- puts_verbose "[Lint Setup]"
12
+ puts_verbose "[Lint Policy]"
12
13
  source_text = source.to_s.strip
13
14
  if source_text.empty?
14
- puts_line "ERROR: lint setup requires --source <path-or-git-url>."
15
+ puts_line "ERROR: lint policy requires --source <path-or-git-url>."
15
16
  return EXIT_ERROR
16
17
  end
17
18
 
@@ -19,40 +20,26 @@ module Carson
19
20
  ref_text = "main" if ref_text.empty?
20
21
  source_dir, cleanup = lint_setup_source_directory( source: source_text, ref: ref_text )
21
22
  begin
22
- source_coding_dir = File.join( source_dir, "CODING" )
23
- unless Dir.exist?( source_coding_dir )
24
- puts_line "ERROR: source CODING directory not found at #{source_coding_dir}."
25
- return EXIT_ERROR
26
- end
27
- target_coding_dir = ai_coding_dir
28
- copy_result = copy_lint_coding_tree(
29
- source_coding_dir: source_coding_dir,
30
- target_coding_dir: target_coding_dir,
23
+ target_dir = repo_linters_dir
24
+ copy_result = copy_lint_policy_files(
25
+ source_dir: source_dir,
26
+ target_dir: target_dir,
31
27
  force: force
32
28
  )
33
- puts_verbose "lint_setup_source: #{source_text}"
34
- puts_verbose "lint_setup_ref: #{ref_text}" if lint_source_git_url?( source: source_text )
35
- puts_verbose "lint_setup_target: #{target_coding_dir}"
36
- puts_verbose "lint_setup_created: #{copy_result.fetch( :created )}"
37
- puts_verbose "lint_setup_updated: #{copy_result.fetch( :updated )}"
38
- puts_verbose "lint_setup_skipped: #{copy_result.fetch( :skipped )}"
39
-
40
- missing_policy = missing_lint_policy_files
41
- if missing_policy.empty?
42
- puts_line "OK: lint policy setup is complete."
43
- return EXIT_OK
44
- end
45
-
46
- missing_policy.each do |entry|
47
- puts_verbose "missing_lint_policy_file: language=#{entry.fetch( :language )} path=#{entry.fetch( :path )}"
48
- end
49
- puts_line "ACTION: update source CODING policy files, rerun carson lint setup, then rerun carson audit."
50
- EXIT_ERROR
29
+ puts_verbose "lint_policy_source: #{source_text}"
30
+ puts_verbose "lint_policy_ref: #{ref_text}" if lint_source_git_url?( source: source_text )
31
+ puts_verbose "lint_policy_target: #{target_dir}"
32
+ puts_verbose "lint_policy_created: #{copy_result.fetch( :created )}"
33
+ puts_verbose "lint_policy_updated: #{copy_result.fetch( :updated )}"
34
+ puts_verbose "lint_policy_skipped: #{copy_result.fetch( :skipped )}"
35
+
36
+ puts_line "OK: lint policy synced to .github/linters/ (#{copy_result.fetch( :created )} created, #{copy_result.fetch( :updated )} updated)."
37
+ EXIT_OK
51
38
  ensure
52
39
  cleanup&.call
53
40
  end
54
41
  rescue StandardError => e
55
- puts_line "ERROR: lint setup failed (#{e.message})"
42
+ puts_line "ERROR: lint policy failed (#{e.message})"
56
43
  EXIT_ERROR
57
44
  end
58
45
 
@@ -119,22 +106,21 @@ module Carson
119
106
  "/tmp/carson"
120
107
  end
121
108
 
122
- def ai_coding_dir
123
- home = ENV.fetch( "HOME", "" ).to_s.strip
124
- raise "HOME must be an absolute path for lint setup" unless home.start_with?( "/" )
125
-
126
- File.join( home, ".carson", "lint" )
109
+ # Lint configs live inside the governed repository for MegaLinter.
110
+ def repo_linters_dir
111
+ File.join( repo_root, ".github", "linters" )
127
112
  end
128
113
 
129
- def copy_lint_coding_tree( source_coding_dir:, target_coding_dir:, force: )
130
- FileUtils.mkdir_p( target_coding_dir )
114
+ def copy_lint_policy_files( source_dir:, target_dir:, force: )
115
+ FileUtils.mkdir_p( target_dir )
131
116
  created = 0
132
117
  updated = 0
133
118
  skipped = 0
134
- Dir.glob( "**/*", File::FNM_DOTMATCH, base: source_coding_dir ).sort.each do |relative|
119
+ Dir.glob( "**/*", File::FNM_DOTMATCH, base: source_dir ).sort.each do |relative|
135
120
  next if [ ".", ".." ].include?( relative )
136
- source_path = File.join( source_coding_dir, relative )
137
- target_path = File.join( target_coding_dir, relative )
121
+ next if relative.start_with?( ".git/" ) || relative == ".git"
122
+ source_path = File.join( source_dir, relative )
123
+ target_path = File.join( target_dir, relative )
138
124
  if File.directory?( source_path )
139
125
  FileUtils.mkdir_p( target_path )
140
126
  next
@@ -161,16 +147,6 @@ module Carson
161
147
  skipped: skipped
162
148
  }
163
149
  end
164
-
165
- def missing_lint_policy_files
166
- config.lint_languages.each_with_object( [] ) do |( language, entry ), missing|
167
- next unless entry.fetch( :enabled )
168
-
169
- entry.fetch( :config_files ).each do |path|
170
- missing << { language: language, path: path } unless File.file?( path )
171
- end
172
- end
173
- end
174
150
  end
175
151
 
176
152
  include Lint
@@ -371,7 +371,11 @@ module Carson
371
371
 
372
372
  # Calculates whole-file expected content and returns sync status plus apply payload.
373
373
  def template_result_for_file( managed_file: )
374
- template_path = File.join( github_templates_dir, File.basename( managed_file ) )
374
+ # Try subdirectory-aware path first (e.g. .github/workflows/carson-lint.yml),
375
+ # then fall back to flat basename lookup for backward compatibility.
376
+ relative_within_github = managed_file.delete_prefix( ".github/" )
377
+ template_path = File.join( github_templates_dir, relative_within_github )
378
+ template_path = File.join( github_templates_dir, File.basename( managed_file ) ) unless File.file?( template_path )
375
379
  return { file: managed_file, status: "error", reason: "missing template #{File.basename( managed_file )}", applied_content: nil } unless File.file?( template_path )
376
380
 
377
381
  expected_content = normalize_text( text: File.read( template_path ) )
@@ -39,6 +39,12 @@ module Carson
39
39
  merge_choice = prompt_merge_method
40
40
  choices[ "govern.merge.method" ] = merge_choice unless merge_choice.nil?
41
41
 
42
+ lint_command_choice = prompt_lint_command
43
+ choices[ "lint.command" ] = lint_command_choice unless lint_command_choice.nil?
44
+
45
+ lint_enforcement_choice = prompt_lint_enforcement
46
+ choices[ "lint.enforcement" ] = lint_enforcement_choice unless lint_enforcement_choice.nil?
47
+
42
48
  write_setup_config( choices: choices )
43
49
  end
44
50
 
@@ -142,6 +148,34 @@ module Carson
142
148
  prompt_choice( options: options, default: 0 )
143
149
  end
144
150
 
151
+ def prompt_lint_command
152
+ puts_line ""
153
+ puts_line "Lint command"
154
+ options = [
155
+ { label: "make lint", value: "make lint" },
156
+ { label: "trunk check (Recommended)", value: "trunk check" },
157
+ { label: "Other (enter command)", value: :other },
158
+ { label: "Skip (no local lint)", value: nil }
159
+ ]
160
+ choice = prompt_choice( options: options, default: 1 )
161
+
162
+ if choice == :other
163
+ prompt_custom_value( label: "Lint command" )
164
+ else
165
+ choice
166
+ end
167
+ end
168
+
169
+ def prompt_lint_enforcement
170
+ puts_line ""
171
+ puts_line "Lint enforcement"
172
+ options = [
173
+ { label: "strict — block on lint failure (default)", value: "strict" },
174
+ { label: "advisory — warn but don't block", value: "advisory" }
175
+ ]
176
+ prompt_choice( options: options, default: 0 )
177
+ end
178
+
145
179
  def prompt_choice( options:, default: )
146
180
  options.each_with_index do |option, index|
147
181
  puts_line " #{index + 1}) #{option.fetch( :label )}"
@@ -0,0 +1,23 @@
1
+ name: Carson Lint
2
+ on:
3
+ pull_request:
4
+ branches: [main, master]
5
+ push:
6
+ branches: [main, master]
7
+
8
+ jobs:
9
+ lint:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: read
13
+ issues: write
14
+ pull-requests: write
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ with:
18
+ fetch-depth: 0
19
+ - uses: oxsecurity/megalinter@v8
20
+ env:
21
+ VALIDATE_ALL_CODEBASE: false
22
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23
+ LINTER_RULES_PATH: .github/linters
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.11.2
4
+ version: 2.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang
@@ -9,10 +9,10 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: Carson is an autonomous governance runtime that lives outside your repositories.
13
- It enforces lint policy on every commit, gates merges on unresolved reviewer feedback,
14
- triages open PRs across your entire portfolio, dispatches coding agents to fix failures,
15
- merges what's ready, and cleans up after itself. One gem, all your projects, unmanned.
12
+ description: Carson lives outside the repositories it governs. On every commit it
13
+ enforces centralised lint policy and scope checks. On PRs it gates merge on unresolved
14
+ reviewer feedback, dispatches coding agents to fix CI failures, merges passing PRs,
15
+ and housekeeps branches. Runs locally and in GitHub Actions.
16
16
  email:
17
17
  - wanghailei@users.noreply.github.com
18
18
  executables:
@@ -46,7 +46,6 @@ files:
46
46
  - lib/carson/adapters/prompt.rb
47
47
  - lib/carson/cli.rb
48
48
  - lib/carson/config.rb
49
- - lib/carson/policy/ruby/lint.rb
50
49
  - lib/carson/runtime.rb
51
50
  - lib/carson/runtime/audit.rb
52
51
  - lib/carson/runtime/govern.rb
@@ -65,6 +64,7 @@ files:
65
64
  - templates/.github/carson-instructions.md
66
65
  - templates/.github/copilot-instructions.md
67
66
  - templates/.github/pull_request_template.md
67
+ - templates/.github/workflows/carson-lint.yml
68
68
  homepage: https://github.com/wanghailei/carson
69
69
  licenses:
70
70
  - MIT
@@ -93,5 +93,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
93
  requirements: []
94
94
  rubygems_version: 4.0.3
95
95
  specification_version: 4
96
- summary: You write the code, Carson manages everything from commit to merge.
96
+ summary: Autonomous governance runtime — lint, review gates, PR triage, and merge
97
+ across repositories.
97
98
  test_files: []
@@ -1,60 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "open3"
4
-
5
- EXIT_OK = 0
6
- EXIT_ERROR = 1
7
- EXIT_BLOCK = 2
8
-
9
- def rubocop_config_path
10
- File.expand_path( "~/.carson/lint/rubocop.yml" )
11
- end
12
-
13
- def print_stream( io, text )
14
- content = text.to_s
15
- return if content.empty?
16
- io.print( content )
17
- end
18
-
19
- def run_rubocop( files: )
20
- stdout_text, stderr_text, status = Open3.capture3(
21
- "rubocop", "--config", rubocop_config_path, *files
22
- )
23
- print_stream( $stdout, stdout_text )
24
- print_stream( $stderr, stderr_text )
25
- { status: :completed, exit_code: status.exitstatus.to_i }
26
- rescue Errno::ENOENT
27
- $stderr.puts "ERROR: RuboCop executable `rubocop` is unavailable in PATH. Install the pinned RuboCop gem before running carson audit."
28
- { status: :unavailable, exit_code: nil }
29
- rescue StandardError => e
30
- $stderr.puts "ERROR: RuboCop execution failed (#{e.message})"
31
- { status: :runtime_error, exit_code: nil }
32
- end
33
-
34
- def lint_exit_code( result: )
35
- case result.fetch( :status )
36
- when :unavailable
37
- EXIT_BLOCK
38
- when :runtime_error
39
- EXIT_ERROR
40
- else
41
- case result.fetch( :exit_code )
42
- when 0
43
- EXIT_OK
44
- when 1
45
- EXIT_BLOCK
46
- else
47
- EXIT_ERROR
48
- end
49
- end
50
- end
51
-
52
- files = ARGV.map( &:to_s ).map( &:strip ).reject( &:empty? )
53
- config_path = rubocop_config_path
54
- unless File.file?( config_path )
55
- $stderr.puts "ERROR: RuboCop config not found at #{config_path}. Run `carson lint setup --source <path-or-git-url>`."
56
- exit EXIT_ERROR
57
- end
58
-
59
- result = run_rubocop( files: files )
60
- exit lint_exit_code( result: result )