Module: Carson::Runtime::Local

Included in:
Carson::Runtime
Defined in:
lib/carson/runtime/local/sync.rb,
lib/carson/runtime/local/hooks.rb,
lib/carson/runtime/local/prune.rb,
lib/carson/runtime/local/onboard.rb,
lib/carson/runtime/local/template.rb,
lib/carson/runtime/local/worktree.rb

Constant Summary collapse

TEMPLATE_SYNC_BRANCH =
"carson/template-sync".freeze
SUPERSEDED =
[
	".github/carson-instructions.md",
	".github/biome.json",
	".github/erb-lint.yml",
	".github/rubocop.yml",
	".github/ruff.toml",
	".github/workflows/carson-lint.yml",
	".github/.mega-linter.yml",
".github/carson.md",
".github/copilot-instructions.md",
".github/CLAUDE.md",
".github/AGENTS.md",
".github/pull_request_template.md"
].freeze

Instance Method Summary collapse

Instance Method Details

#cwd_worktree_branchObject

Returns the branch checked out in the worktree that contains the process CWD, or nil if CWD is not inside any worktree. Used by prune to proactively protect the CWD worktree’s branch from deletion. Matches the longest (most specific) path because worktree directories live under the main repo tree (.claude/worktrees/).



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/carson/runtime/local/worktree.rb', line 38

def cwd_worktree_branch
	cwd = realpath_safe( Dir.pwd )
	best_branch = nil
	best_length = -1
	worktree_list.each do |worktree|
		normalised = File.join( worktree.path, "" )
		if ( cwd == worktree.path || cwd.start_with?( normalised ) ) && worktree.path.length > best_length
			best_branch = worktree.branch
			best_length = worktree.path.length
		end
	end
	best_branch
rescue StandardError
	nil
end

#main_worktree_rootObject

Returns the main (non-worktree) repository root. Uses git-common-dir to find the shared .git directory, then takes its parent. Falls back to repo_root if detection fails.



57
58
59
60
61
62
# File 'lib/carson/runtime/local/worktree.rb', line 57

def main_worktree_root
	common_dir, _, success, = git_run( "rev-parse", "--path-format=absolute", "--git-common-dir" )
	return File.dirname( common_dir.strip ) if success && !common_dir.strip.empty?

	repo_root
end

#offboard!Object

Removes Carson-managed repository integration so a host repository can retire Carson cleanly.



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/carson/runtime/local/onboard.rb', line 198

def offboard!
	puts_verbose ""
	puts_verbose "[Offboard]"
	unless inside_git_work_tree?
		puts_line "#{repo_root} is not a git repository."
		return EXIT_ERROR
	end
	if input_stream.respond_to?( :tty? ) && input_stream.tty?
		puts_line ""
		puts_line "This will remove Carson hooks, managed .github/ files,"
		puts_line "and deregister this repository from portfolio governance."
		puts_line "Continue?"
		unless prompt_yes_no( default: false )
			puts_line "Offboard cancelled."
			return EXIT_OK
		end
	end

	hooks_status = disable_carson_hooks_path!
	return hooks_status unless hooks_status == EXIT_OK

	removed_count = 0
	missing_count = 0
	offboard_cleanup_targets.each do |relative|
		absolute = resolve_repo_path!( relative_path: relative, label: "offboard target #{relative}" )
		if File.exist?( absolute )
			FileUtils.rm_rf( absolute )
			puts_verbose "removed_path: #{relative}"
			removed_count += 1
		else
			puts_verbose "skip_missing_path: #{relative}"
			missing_count += 1
		end
	end
	remove_empty_offboard_directories!
	remove_govern_repo!( repo_path: File.expand_path( repo_root ) )
	puts_verbose "govern_deregistered: #{File.expand_path( repo_root )}"
	puts_verbose "offboard_summary: removed=#{removed_count} missing=#{missing_count}"
	if verbose?
		puts_line "OK: Carson offboard completed for #{repo_root}."
	else
		puts_line "Removed #{removed_count} file#{plural_suffix( count: removed_count )}. Offboard complete."
	end
	puts_line ""
	puts_line "Next: commit the removals and push to finalise offboarding."
	EXIT_OK
end

#onboard!Object

One-command onboarding for new repositories: detect remote, install hooks, apply templates, and run initial audit.



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/carson/runtime/local/onboard.rb', line 10

def onboard!
	fingerprint_status = block_if_outsider_fingerprints!
	return fingerprint_status unless fingerprint_status.nil?

	unless inside_git_work_tree?
		puts_line "#{repo_root} is not a git repository."
		return EXIT_ERROR
	end

	repo_name = File.basename( repo_root )
	puts_line ""
	puts_line "Onboarding #{repo_name}..."

	if !global_config_exists? || !git_remote_exists?( remote_name: config.git_remote )
		if input_stream.respond_to?( :tty? ) && input_stream.tty?
			setup_status = setup!
			return setup_status unless setup_status == EXIT_OK
		else
			silent_setup!
		end
	end

	onboard_apply!
end

#prune!(json_output: false) ⇒ Object



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/carson/runtime/local/prune.rb', line 69

def prune!( json_output: false )
	fingerprint_status = block_if_outsider_fingerprints!
	unless fingerprint_status.nil?
		if json_output
			output.puts JSON.pretty_generate( {
				command: "prune", status: "block",
				error: "Carson-owned artefacts detected in host repository",
				recovery: "remove Carson-owned files (.carson.yml, bin/carson, .tools/carson) then retry",
				exit_code: EXIT_BLOCK
			} )
		end
		return fingerprint_status
	end

	prune_git!( "fetch", config.git_remote, "--prune", json_output: json_output )

	# Clean stale worktree entries whose directories no longer exist.
	# Unblocks branch deletion for branches held by dead worktrees.
	git_run( "worktree", "prune" )

	active_branch = current_branch
	cwd_branch = cwd_worktree_branch
	counters = { deleted: 0, skipped: 0 }
	branches = []

	stale_branches = stale_local_branches
	prune_stale_branch_entries( stale_branches: stale_branches, active_branch: active_branch, cwd_branch: cwd_branch, counters: counters, branches: branches )

	orphan_branches = orphan_local_branches( active_branch: active_branch, cwd_branch: cwd_branch )
	prune_orphan_branch_entries( orphan_branches: orphan_branches, counters: counters, branches: branches )

	absorbed_branches = absorbed_local_branches( active_branch: active_branch, cwd_branch: cwd_branch )
	prune_absorbed_branch_entries( absorbed_branches: absorbed_branches, counters: counters, branches: branches )

	prune_finish(
		result: { command: "prune", status: "ok", branches: branches, deleted: counters.fetch( :deleted ), skipped: counters.fetch( :skipped ) },
		exit_code: EXIT_OK, json_output: json_output, counters: counters
	)
end

#prune_all!Object



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/carson/runtime/local/onboard.rb', line 147

def prune_all!
	repos = config.govern_repos
	if repos.empty?
		puts_line "No governed repositories configured."
		puts_line "  Run carson onboard in each repo to register."
		return EXIT_ERROR
	end

	puts_line ""
	puts_line "Prune all (#{repos.length} repo#{plural_suffix( count: repos.length )})"
	succeeded = 0
	failed = 0

	repos.each do |repo_path|
		repo_name = File.basename( repo_path )
		unless Dir.exist?( repo_path )
			puts_line "#{repo_name}: not found"
			record_batch_skip( command: "prune", repo_path: repo_path, reason: "path not found" )
			failed += 1
			next
		end

		begin
			buffer = verbose? ? output : StringIO.new
			error_buffer = verbose? ? error : StringIO.new
			scoped_runtime = Runtime.new( repo_root: repo_path, tool_root: tool_root, output: buffer, error: error_buffer, verbose: verbose? )
			status = scoped_runtime.prune!
			unless verbose?
				summary = buffer.string.lines.last.to_s.strip
				puts_line "#{repo_name}: #{summary.empty? ? 'OK' : summary}"
			end
			if status == EXIT_ERROR
				record_batch_skip( command: "prune", repo_path: repo_path, reason: "prune failed" )
				failed += 1
			else
				clear_batch_success( command: "prune", repo_path: repo_path )
				succeeded += 1
			end
		rescue StandardError => exception
			puts_line "#{repo_name}: could not complete (#{exception.message})"
			record_batch_skip( command: "prune", repo_path: repo_path, reason: exception.message )
			failed += 1
		end
	end

	puts_line ""
	puts_line "Prune all complete: #{succeeded} pruned, #{failed} failed."
	failed.zero? ? EXIT_OK : EXIT_ERROR
end

#prune_plan(dry_run: true) ⇒ Object

Returns a plan hash describing what prune! would do, without executing any mutations. Does NOT fetch — branch staleness reflects whatever the last fetch left behind. Returns: { stale: […], orphan: […], absorbed: […] } Each item: { branch:, action: :delete|:skip, reason:, type: }



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/carson/runtime/local/prune.rb', line 11

def prune_plan( dry_run: true ) # rubocop:disable Lint/UnusedMethodArgument
	active_branch = current_branch
	cwd_branch = cwd_worktree_branch

	stale = stale_local_branches.map do |entry|
		branch   = entry.fetch( :branch )
		upstream = entry.fetch( :upstream )
		if config.protected_branches.include?( branch )
			{ action: :skip, branch: branch, upstream: upstream, name: branch, type: "stale", reason: "protected branch" }
		elsif branch == active_branch
			{ action: :skip, branch: branch, upstream: upstream, name: branch, type: "stale", reason: "current branch" }
		elsif cwd_branch && branch == cwd_branch
			{ action: :skip, branch: branch, upstream: upstream, name: branch, type: "stale", reason: "checked out in CWD worktree" }
		else
			{ action: :delete, branch: branch, upstream: upstream, name: branch, type: "stale", reason: "upstream gone" }
		end
	end

	orphan = orphan_local_branches( active_branch: active_branch, cwd_branch: cwd_branch ).map do |branch|
		if gh_available?
			tip_sha = begin
				git_capture!( "rev-parse", "--verify", branch ).strip
			rescue StandardError
				nil
			end

			if tip_sha
				merged_pr, = merged_pr_for_branch( branch: branch, branch_tip_sha: tip_sha )
				if merged_pr.nil? && branch_absorbed_into_main?( branch: branch )
					merged_pr = { url: "absorbed into #{config.main_branch}" }
				end

				if merged_pr
					{ action: :delete, branch: branch, upstream: "", name: branch, type: "orphan", reason: "merged — #{merged_pr[ :url ]}" }
				else
					{ action: :skip, branch: branch, upstream: "", name: branch, type: "orphan", reason: "no merged PR evidence" }
				end
			else
				{ action: :skip, branch: branch, upstream: "", name: branch, type: "orphan", reason: "cannot read branch tip SHA" }
			end
		else
			{ action: :skip, branch: branch, upstream: "", name: branch, type: "orphan", reason: "gh CLI not available" }
		end
	end

	absorbed = absorbed_local_branches( active_branch: active_branch, cwd_branch: cwd_branch ).map do |entry|
		branch   = entry.fetch( :branch )
		upstream = entry.fetch( :upstream )
		if gh_available? && branch_has_open_pr?( branch: branch )
			{ action: :skip, branch: branch, upstream: upstream, name: branch, type: "absorbed", reason: "open PR exists" }
		else
			{ action: :delete, branch: branch, upstream: upstream, name: branch, type: "absorbed", reason: "content already on main" }
		end
	end

	{ stale: stale, orphan: orphan, absorbed: absorbed }
end

#realpath_safe(path) ⇒ Object

Resolves a path to its canonical form, tolerating non-existent paths. Preserves canonical parents for missing paths so deleted worktrees still compare equal to git’s recorded path (for example /tmp vs /private/tmp).



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/carson/runtime/local/worktree.rb', line 67

def realpath_safe( path )
	File.realpath( path )
rescue Errno::ENOENT
	expanded = File.expand_path( path )
	missing_segments = []
	candidate = expanded

	until File.exist?( candidate ) || Dir.exist?( candidate )
		parent = File.dirname( candidate )
		break if parent == candidate

		missing_segments.unshift( File.basename( candidate ) )
		candidate = parent
	end

	base = if File.exist?( candidate ) || Dir.exist?( candidate )
		File.realpath( candidate )
	else
		candidate
	end

	missing_segments.empty? ? base : File.join( base, *missing_segments )
end

#refresh!Object

Re-applies hooks, templates, and audit after upgrading Carson.



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/carson/runtime/local/onboard.rb', line 36

def refresh!
	fingerprint_status = block_if_outsider_fingerprints!
	return fingerprint_status unless fingerprint_status.nil?

	unless inside_git_work_tree?
		puts_line "#{repo_root} is not a git repository."
		return EXIT_ERROR
	end

	if verbose?
		puts_verbose ""
		puts_verbose "[Refresh]"
		hook_status = prepare!
		return hook_status unless hook_status == EXIT_OK

		drift_count = template_results.count { |entry| entry.fetch( :status ) != "ok" }
		stale_count = template_superseded_present.count
		template_status = template_apply!
		return template_status unless template_status == EXIT_OK

		@template_sync_result = template_propagate!( drift_count: drift_count + stale_count )

		audit_status = audit!
		if audit_status == EXIT_OK
			puts_line "OK: Carson refresh completed for #{repo_root}."
		elsif audit_status == EXIT_BLOCK
			puts_line "Refresh complete — some checks need attention. Run carson audit for details."
		end
		return audit_status
	end

	puts_line "Refresh"
	hook_status = with_captured_output { prepare! }
	return hook_status unless hook_status == EXIT_OK
	puts_line "Hooks installed (#{config.managed_hooks.count} hooks)."

	template_drift_count = template_results.count { |entry| entry.fetch( :status ) != "ok" }
	stale_count = template_superseded_present.count
	template_status = with_captured_output { template_apply! }
	return template_status unless template_status == EXIT_OK
	total_drift = template_drift_count + stale_count
	if total_drift.positive?
		puts_line "Templates applied (#{template_drift_count} updated, #{stale_count} removed)."
	else
		puts_line "Templates in sync."
	end

	@template_sync_result = template_propagate!( drift_count: total_drift )

	audit_status = audit!
	puts_line "Refresh complete."
	audit_status
end

#refresh_all!Object

Re-applies hooks, templates, and audit across all governed repositories. Checks each repo for safety (active worktrees, uncommitted changes) and marks unsafe repos as pending to avoid disrupting active work.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/carson/runtime/local/onboard.rb', line 93

def refresh_all!
	repos = config.govern_repos
	if repos.empty?
		puts_line "No governed repositories configured."
		puts_line "  Run carson onboard in each repo to register."
		return EXIT_ERROR
	end

	pending_before = pending_repos_for( command: "refresh" )
	if pending_before.any?
		puts_line "#{pending_before.length} repo#{plural_suffix( count: pending_before.length )} pending from previous run"
	end

	puts_line ""
	puts_line "Refresh all (#{repos.length} repo#{plural_suffix( count: repos.length )})"
	refreshed = 0
	pending = 0
	failed = 0

	repos.each do |repo_path|
		repo_name = File.basename( repo_path )
		unless Dir.exist?( repo_path )
			puts_line "#{repo_name}: not found"
			record_batch_skip( command: "refresh", repo_path: repo_path, reason: "path not found" )
			failed += 1
			next
		end

		safety = portfolio_repo_safety( repo_path: repo_path )
		unless safety.fetch( :safe )
			reason = safety.fetch( :reasons ).join( ", " )
			puts_line "#{repo_name}: PENDING (#{reason})"
			record_batch_skip( command: "refresh", repo_path: repo_path, reason: reason )
			pending += 1
			next
		end

		status = refresh_single_repo( repo_path: repo_path, repo_name: repo_name )
		if status == EXIT_ERROR
			failed += 1
		else
			clear_batch_success( command: "refresh", repo_path: repo_path )
			refreshed += 1
		end
	end

	puts_line ""
	parts = [ "#{refreshed} refreshed" ]
	parts << "#{pending} still pending (will retry on next run)" if pending.positive?
	parts << "#{failed} failed" if failed.positive?
	puts_line "Refresh all complete: #{parts.join( ', ' )}."
	failed.zero? && pending.zero? ? EXIT_OK : EXIT_ERROR
end

#sweep_stale_worktrees!Object

Removes agent-owned worktrees whose branch content is already on main.



22
23
24
# File 'lib/carson/runtime/local/worktree.rb', line 22

def sweep_stale_worktrees!
	Worktree.sweep_stale!( runtime: self )
end

#sync!(json_output: false) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/carson/runtime/local/sync.rb', line 6

def sync!( json_output: false )
	fingerprint_status = block_if_outsider_fingerprints!
	return fingerprint_status unless fingerprint_status.nil?

	unless working_tree_clean?
		return sync_finish(
			result: sync_dirty_result,
			exit_code: EXIT_BLOCK, json_output: json_output
		)
	end
	start_branch = current_branch
	switched = false
	sync_git!( "fetch", config.git_remote, "--prune", json_output: json_output )
	if start_branch != config.main_branch
		sync_git!( "switch", config.main_branch, json_output: json_output )
		switched = true
	end
	sync_git!( "pull", "--ff-only", config.git_remote, config.main_branch, json_output: json_output )
	ahead_count, behind_count, error_text = main_sync_counts
	if error_text
		return sync_finish(
			result: { command: "sync", status: "block", error: "unable to verify main sync state (#{error_text})" },
			exit_code: EXIT_BLOCK, json_output: json_output
		)
	end
	if ahead_count.zero? && behind_count.zero?
		return sync_finish(
			result: { command: "sync", status: "ok", ahead: 0, behind: 0, main_branch: config.main_branch, remote: config.git_remote },
			exit_code: EXIT_OK, json_output: json_output
		)
	end
	sync_finish(
		result: { command: "sync", status: "block", ahead: ahead_count, behind: behind_count, main_branch: config.main_branch, remote: config.git_remote, error: "local #{config.main_branch} still diverges" },
		exit_code: EXIT_BLOCK, json_output: json_output
	)
ensure
	git_system!( "switch", start_branch ) if switched && branch_exists?( branch_name: start_branch )
end

#sync_all!Object

Syncs main branch across all governed repositories.



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/carson/runtime/local/sync.rb', line 46

def sync_all!
	repos = config.govern_repos
	if repos.empty?
		puts_line "No governed repositories configured."
		puts_line "  Run carson onboard in each repo to register."
		return EXIT_ERROR
	end

	puts_line ""
	puts_line "Sync all (#{repos.length} repo#{plural_suffix( count: repos.length )})"
	synced = 0
	failed = 0

	repos.each do |repo_path|
		repo_name = File.basename( repo_path )
		unless Dir.exist?( repo_path )
			puts_line "#{repo_name}: not found"
			record_batch_skip( command: "sync", repo_path: repo_path, reason: "path not found" )
			failed += 1
			next
		end

		begin
			scoped_runtime = build_scoped_runtime( repo_path: repo_path )
			status = scoped_runtime.sync!
			if status == EXIT_OK
				puts_line "#{repo_name}: ok" unless verbose?
				clear_batch_success( command: "sync", repo_path: repo_path )
				synced += 1
			else
				puts_line "#{repo_name}: could not sync" unless verbose?
				record_batch_skip( command: "sync", repo_path: repo_path, reason: "sync failed" )
				failed += 1
			end
		rescue StandardError => exception
			puts_line "#{repo_name}: could not sync (#{exception.message})"
			record_batch_skip( command: "sync", repo_path: repo_path, reason: exception.message )
			failed += 1
		end
	end

	puts_line ""
	puts_line "Sync all complete: #{synced} synced, #{failed} failed."
	failed.zero? ? EXIT_OK : EXIT_ERROR
end

#template_apply!(push_prep: false) ⇒ Object

Applies managed template files as full-file writes from Carson sources. Also removes superseded files that are no longer part of the managed set.



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/carson/runtime/local/template.rb', line 105

def template_apply!( push_prep: false )
	fingerprint_status = block_if_outsider_fingerprints!
	return fingerprint_status unless fingerprint_status.nil?

	puts_verbose ""
	puts_verbose "[Template Sync Apply]"
	results = template_results
	stale = template_superseded_present
	applied = 0
	results.each do |entry|
		if entry.fetch( :status ) == "error"
			puts_verbose "template_file: #{entry.fetch( :file )} status=error reason=#{entry.fetch( :reason )}"
			next
		end

		file_path = File.join( repo_root, entry.fetch( :file ) )
		if entry.fetch( :status ) == "ok"
			puts_verbose "template_file: #{entry.fetch( :file )} status=ok reason=in_sync"
			next
		end

		FileUtils.mkdir_p( File.dirname( file_path ) )
		File.write( file_path, entry.fetch( :applied_content ) )
		puts_verbose "template_file: #{entry.fetch( :file )} status=updated reason=#{entry.fetch( :reason )}"
		applied += 1
	end

	removed = 0
	stale.each do |file|
		file_path = resolve_repo_path!( relative_path: file, label: "superseded file #{file}" )
		File.delete( file_path )
		puts_verbose "template_file: #{file} status=removed reason=superseded"
		removed += 1
	end

	error_count = results.count { |entry| entry.fetch( :status ) == "error" }
	puts_verbose "template_apply_summary: updated=#{applied} removed=#{removed} error=#{error_count}"
	unless verbose?
		if applied.positive? || removed.positive?
			summary_parts = []
			summary_parts << "#{applied} updated" if applied.positive?
			summary_parts << "#{removed} removed" if removed.positive?
			puts_line "Templates applied (#{summary_parts.join( ", " )})."
		else
			puts_line "Templates in sync."
		end
	end
	return EXIT_ERROR if error_count.positive?

	return EXIT_BLOCK if push_prep && push_prep_commit!
	EXIT_OK
end

#template_check!Object

Read-only template drift check; returns block when managed files are output of sync.



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/carson/runtime/local/template.rb', line 23

def template_check!
	fingerprint_status = block_if_outsider_fingerprints!
	return fingerprint_status unless fingerprint_status.nil?

	puts_verbose ""
	puts_verbose "[Template Sync Check]"
	results = template_results
	stale = template_superseded_present
	drift_count = results.count { |entry| entry.fetch( :status ) == "drift" }
	error_count = results.count { |entry| entry.fetch( :status ) == "error" }
	stale_count = stale.count
	results.each do |entry|
		puts_verbose "template_file: #{entry.fetch( :file )} status=#{entry.fetch( :status )} reason=#{entry.fetch( :reason )}"
	end
	stale.each { |file| puts_verbose "template_file: #{file} status=stale reason=superseded" }
	puts_verbose "template_summary: total=#{results.count} drift=#{drift_count} stale=#{stale_count} error=#{error_count}"
	unless verbose?
		if ( drift_count + stale_count ).positive?
			summary_parts = []
			summary_parts << "#{drift_count} of #{results.count} drifted" if drift_count.positive?
			summary_parts << "#{stale_count} stale" if stale_count.positive?
			puts_line "Templates: #{summary_parts.join( ", " )}"
			results.select { |entry| entry.fetch( :status ) == "drift" }.each { |entry| puts_line "  #{entry.fetch( :file )}" }
			stale.each { |file| puts_line "  #{file} — superseded" }
		else
			puts_line "Templates: #{results.count} files in sync"
		end
	end
	return EXIT_ERROR if error_count.positive?

	( drift_count + stale_count ).positive? ? EXIT_BLOCK : EXIT_OK
end

#template_check_all!Object

Read-only template drift check across all governed repositories.



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/carson/runtime/local/template.rb', line 57

def template_check_all!
	repos = config.govern_repos
	if repos.empty?
		puts_line "No governed repositories configured."
		puts_line "  Run carson onboard in each repo to register."
		return EXIT_ERROR
	end

	puts_line ""
	puts_line "Template check all (#{repos.length} repo#{plural_suffix( count: repos.length )})"
	in_sync = 0
	drifted = 0
	failed = 0

	repos.each do |repo_path|
		repo_name = File.basename( repo_path )
		unless Dir.exist?( repo_path )
			puts_line "#{repo_name}: not found"
			record_batch_skip( command: "template_check", repo_path: repo_path, reason: "path not found" )
			failed += 1
			next
		end

		begin
			scoped_runtime = build_scoped_runtime( repo_path: repo_path )
			status = scoped_runtime.template_check!
			if status == EXIT_OK
				puts_line "#{repo_name}: in sync" unless verbose?
				clear_batch_success( command: "template_check", repo_path: repo_path )
				in_sync += 1
			else
				puts_line "#{repo_name}: DRIFT" unless verbose?
				drifted += 1
			end
		rescue StandardError => exception
			puts_line "#{repo_name}: could not complete (#{exception.message})"
			record_batch_skip( command: "template_check", repo_path: repo_path, reason: exception.message )
			failed += 1
		end
	end

	puts_line ""
	puts_line "Template check complete: #{in_sync} in sync, #{drifted} drifted, #{failed} failed."
	drifted.zero? && failed.zero? ? EXIT_OK : EXIT_BLOCK
end

#worktree_create!(name:, json_output: false) ⇒ Object

Creates a new worktree under .claude/worktrees/<name>.



12
13
14
# File 'lib/carson/runtime/local/worktree.rb', line 12

def worktree_create!( name:, json_output: false )
	Worktree.create!( name: name, runtime: self, json_output: json_output )
end

#worktree_listObject

Returns all registered worktrees as Carson::Worktree instances.



27
28
29
# File 'lib/carson/runtime/local/worktree.rb', line 27

def worktree_list
	Worktree.list( runtime: self )
end

#worktree_remove!(worktree_path:, force: false, json_output: false) ⇒ Object

Removes a worktree: directory, git registration, and branch.



17
18
19
# File 'lib/carson/runtime/local/worktree.rb', line 17

def worktree_remove!( worktree_path:, force: false, json_output: false )
	Worktree.remove!( path: worktree_path, runtime: self, force: force, json_output: json_output )
end