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,
lib/carson/runtime/local/merge_proof.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

#branch_absorbed_into_main?(branch:) ⇒ Boolean

Returns true when the branch has no unique content relative to main.

Returns:

  • (Boolean)


315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/carson/runtime/local/prune.rb', line 315

def branch_absorbed_into_main?( branch: )
	# Fast path: branch is a strict ancestor of main (fully merged).
	_, _, is_ancestor, = git_run( "merge-base", "--is-ancestor", branch, config.main_branch )
	return true if is_ancestor

	# Find the merge-base between main and the branch.
	merge_base_text, _, mb_success, = git_run( "merge-base", config.main_branch, branch )
	return false unless mb_success

	merge_base = merge_base_text.to_s.strip
	return false if merge_base.empty?

	# List every file the branch changed relative to the merge-base.
	changed_text, _, changed_success, = git_run( "diff", "--name-only", merge_base, branch )
	return false unless changed_success

	changed_files = changed_text.to_s.strip.lines.map( &:strip ).reject( &:empty? )
	return true if changed_files.empty?

	# Compare only those files between branch tip and main tip.
	# If identical, every branch change is already on main.
	_, _, identical, = git_run( "diff", "--quiet", branch, config.main_branch, "--", *changed_files )
	identical
end

#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/).



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/carson/runtime/local/worktree.rb', line 57

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.



76
77
78
79
80
81
# File 'lib/carson/runtime/local/worktree.rb', line 76

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

#merge_proof_for_branch(branch:, main_ref: config.main_branch) ⇒ Object



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/carson/runtime/local/merge_proof.rb', line 23

def merge_proof_for_branch( branch:, main_ref: config.main_branch )
	return merge_proof_not_applicable( main_ref: main_ref ) if branch.to_s == main_ref.to_s

	candidate = merge_proof_candidate( branch: branch, main_ref: main_ref )
	return candidate if candidate.fetch( :basis ) == "unavailable"

	trust = merge_proof_main_trust( main_ref: main_ref )
	return candidate if trust.fetch( :trusted )

	merge_proof_hash(
		applicable: true,
		proven: false,
		basis: "unavailable",
		summary: trust.fetch( :summary ),
		main_branch: main_ref,
		changed_files_count: candidate.fetch( :changed_files_count, 0 )
	)
end

#merge_proof_for_remote_ref(branch:, remote: config.git_remote, main_ref: config.main_branch) ⇒ Object

Generates merge proof against the remote tracking ref directly. Skips the local-main trust check — the caller is responsible for fetching before calling. Used by govern’s post-merge path to avoid mutating the main worktree.



9
10
11
12
13
14
15
16
17
18
19
20
21
# File 'lib/carson/runtime/local/merge_proof.rb', line 9

def merge_proof_for_remote_ref( branch:, remote: config.git_remote, main_ref: config.main_branch )
	remote_ref = "#{remote}/#{main_ref}"
	return merge_proof_not_applicable( main_ref: main_ref ) if branch.to_s == main_ref.to_s

	candidate = merge_proof_candidate( branch: branch, main_ref: remote_ref )
	return candidate if candidate.fetch( :basis ) == "unavailable"

	# Normalise display: show the local branch name, not the remote tracking ref.
	candidate.merge(
		main_branch: main_ref,
		summary: candidate.fetch( :summary ).gsub( remote_ref, main_ref )
	)
end

#offboard!Object

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



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 148

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!
	canonical_root = realpath_safe( main_worktree_root )
	remove_govern_repo!( repo_path: canonical_root )
	puts_verbose "govern_deregistered: #{canonical_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 "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_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).



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/carson/runtime/local/worktree.rb', line 86

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/carson/runtime/local/sync.rb', line 6

def sync!( json_output: false )
	# Sync always operates on the main worktree. When called from inside
	# a worktree, delegate to a runtime rooted at the main tree so
	# git switch main does not collide with the main tree's checkout.
	main_root = main_worktree_root
	if realpath_safe( repo_root ) != realpath_safe( main_root )
		main_runtime = Runtime.new( repo_root: main_root, tool_root: tool_root, output: output, error: error, verbose: verbose? )
		return main_runtime.sync!( json_output: json_output )
	end

	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

	attachment = ensure_main_attached!
	unless attachment.fetch( :ok )
		return sync_finish(
			result: { command: "sync", status: "block", error: attachment.fetch( :error ), recovery: attachment[ :recovery ] },
			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

#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.



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

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

#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_list!(json_output: false) ⇒ Object

Human and JSON status surface for all registered worktrees.



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/carson/runtime/local/worktree.rb', line 32

def worktree_list!( json_output: false )
	entries = worktree_inventory
	result = {
		command: "worktree list",
		status: "ok",
		worktrees: entries,
		exit_code: EXIT_OK
	}

	if json_output
		output.puts JSON.pretty_generate( result )
	else
		print_worktree_list( entries: entries )
	end

	EXIT_OK
end

#worktree_remove!(worktree_path:, force: false, skip_unpushed: 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, skip_unpushed: false, json_output: false )
	Worktree.remove!( path: worktree_path, runtime: self, force: force, skip_unpushed: skip_unpushed, json_output: json_output )
end