@danmoisan/drm-copilot-mcp 1.1.14 → 1.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +4 -5
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget-route.ps1 +137 -0
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +95 -66
- package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-CiGateParser.ps1 +330 -0
- package/resources/claude-customizations/.claude/rules/powershell.md +3 -3
- package/resources/claude-customizations/.claude/rules/shell.md +8 -7
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +8 -8
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +234 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_actions_lib.sh +451 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_detached_lib.sh +301 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_dirt_lib.sh +495 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +252 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_lib.sh +496 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_eol_lib.sh +212 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +492 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +476 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +182 -0
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +1 -1
- package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +3 -4
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +1 -1
- package/resources/claude-customizations/.claude/skills/powershell-change-budget-router/SKILL.md +6 -6
- package/resources/claude-customizations/pack-manifests/core.json +11 -0
- package/resources/claude-customizations/pack-manifests/powershell.json +1 -0
- package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +1 -2
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +285 -80
- package/resources/customizations/.github/agents/powershell-orchestrator.agent.md +7 -7
- package/resources/customizations/.github/agents/powershell-typed-engineer.agent.md +8 -10
- package/resources/customizations/.github/prompts/orchestrate-powershell-work.prompt.md +2 -2
- package/resources/customizations/.github/skills/powershell-change-budget-router/SKILL.md +3 -3
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +6 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# cleanup_worktrees_preserve_lib.sh: sourceable preserve-file consolidation function
|
|
3
|
+
# group for the cleanup-worktrees tool. It consumes the `preserved_files[]` array of the
|
|
4
|
+
# manifest the cleanup-merged-worktrees skill writes, validates every record fail-closed,
|
|
5
|
+
# scans the incoming bytes for host-identifying tokens, re-derives each destination
|
|
6
|
+
# index's line-ending convention, and stages the surviving files onto the consolidation
|
|
7
|
+
# branch.
|
|
8
|
+
#
|
|
9
|
+
# Sourcing contract: this library defines functions only; it never runs work at source
|
|
10
|
+
# time, so the wrapper and the bats suites can source it without side effects. It MUST be
|
|
11
|
+
# sourced AFTER cleanup_worktrees_enumerate_lib.sh, cleanup_worktrees_lib.sh, and
|
|
12
|
+
# cleanup_worktrees_actions_lib.sh, because it calls cleanup_wt_git (defined in the
|
|
13
|
+
# enumeration library) and consolidation_worktree_path (defined in the actions library).
|
|
14
|
+
#
|
|
15
|
+
# Two-phase design. preserve_plan performs NO writes: it reads and validates the manifest
|
|
16
|
+
# and every record, verifies each source file exists, runs the host-token pre-pass,
|
|
17
|
+
# decides each index outcome, and emits the PRESERVE| and ACTION| records plus an internal
|
|
18
|
+
# plan stream. preserve_commit_plan is the only function that writes and is entered only
|
|
19
|
+
# when the pre-pass reported no match, which is what keeps the writing phase thin enough
|
|
20
|
+
# to leave in the coverage denominator rather than exclude from measurement.
|
|
21
|
+
#
|
|
22
|
+
# Seams. All git commands go through cleanup_wt_git (CLEANUP_WT_GIT_BIN); jq resolves
|
|
23
|
+
# through preserve_resolve_jq (CLEANUP_WT_JQ_BIN); the manifest path comes from
|
|
24
|
+
# CLEANUP_WT_MANIFEST_PATH, defaulting to
|
|
25
|
+
# artifacts/orchestration/cleanup-worktrees-manifest.json; the consolidation worktree
|
|
26
|
+
# resolves through consolidation_worktree_path (CLEANUP_WT_CONSOLIDATION_PATH).
|
|
27
|
+
#
|
|
28
|
+
# Nothing upstream is trusted. line_ending is always re-derived from the target's current
|
|
29
|
+
# bytes, and the local content scan runs on every record whose advisory result is `clean`.
|
|
30
|
+
# Both advisory values are compared against the local answer and a disagreement is
|
|
31
|
+
# reported, never obeyed.
|
|
32
|
+
#
|
|
33
|
+
# The source file is read with plain file I/O and never through git: the repository-root
|
|
34
|
+
# .claude/agent-memory tree is gitignored, so a git-mediated read would need a force flag.
|
|
35
|
+
# No cleanup_wt_git invocation in this library passes -f or --force ever.
|
|
36
|
+
|
|
37
|
+
preserve_resolve_jq() {
|
|
38
|
+
# Echo the path of the jq binary to use, honoring the CLEANUP_WT_JQ_BIN override seam.
|
|
39
|
+
#
|
|
40
|
+
# Mirrors the resolution and 127 contract of cleanup_wt_git exactly: when
|
|
41
|
+
# CLEANUP_WT_JQ_BIN is set to a non-empty value it must point to an existing
|
|
42
|
+
# executable; an empty or nonexistent value is treated as missing and falls back to
|
|
43
|
+
# `command -v jq`. When neither resolves, a diagnostic naming the tool is written to
|
|
44
|
+
# stderr and the function returns 127 with no stdout, so a missing jq is a loud
|
|
45
|
+
# failure and never a silent no-op that stages nothing while reporting success.
|
|
46
|
+
#
|
|
47
|
+
# `command -v` is a bash builtin, so this function needs no external tool on PATH and
|
|
48
|
+
# the unresolvable case is reachable in a test by pointing PATH at a directory that
|
|
49
|
+
# holds no jq.
|
|
50
|
+
local override=${CLEANUP_WT_JQ_BIN:-}
|
|
51
|
+
local jq_bin=""
|
|
52
|
+
if [[ -n $override && -x $override ]]; then
|
|
53
|
+
jq_bin=$override
|
|
54
|
+
else
|
|
55
|
+
jq_bin=$(command -v jq 2>/dev/null) || jq_bin=""
|
|
56
|
+
fi
|
|
57
|
+
if [[ -z $jq_bin ]]; then
|
|
58
|
+
printf 'cleanup-worktrees: no jq binary resolved (CLEANUP_WT_JQ_BIN=%s)\n' "$override" >&2
|
|
59
|
+
return 127
|
|
60
|
+
fi
|
|
61
|
+
printf '%s\n' "$jq_bin"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
preserve_split_tsv() {
|
|
65
|
+
# Split one tab-separated record into the global array PRESERVE_FIELDS, preserving
|
|
66
|
+
# empty fields exactly.
|
|
67
|
+
#
|
|
68
|
+
# `IFS=$'\t' read -r ...` is deliberately NOT used here. Tab is an IFS *whitespace*
|
|
69
|
+
# character, so read collapses runs of tabs and strips leading and trailing ones; a
|
|
70
|
+
# record whose memory_index_line column is empty would then shift every later column
|
|
71
|
+
# left by one and the evidence field would land in the pattern-set-id variable. This
|
|
72
|
+
# splitter walks the string with parameter expansion instead, which treats an empty
|
|
73
|
+
# field as a field.
|
|
74
|
+
#
|
|
75
|
+
# Args: $1 = the tab-separated record.
|
|
76
|
+
local rest=${1-}
|
|
77
|
+
PRESERVE_FIELDS=()
|
|
78
|
+
while [[ $rest == *$'\t'* ]]; do
|
|
79
|
+
PRESERVE_FIELDS+=("${rest%%$'\t'*}")
|
|
80
|
+
rest=${rest#*$'\t'}
|
|
81
|
+
done
|
|
82
|
+
PRESERVE_FIELDS+=("$rest")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
preserve_read_manifest() {
|
|
86
|
+
# Emit one tab-separated record per preserved_files[] entry of the named manifest.
|
|
87
|
+
#
|
|
88
|
+
# Exactly one jq invocation does both jobs: it enforces the two top-level checks and
|
|
89
|
+
# projects each record into the fixed 14-column order the rest of this library reads.
|
|
90
|
+
# The columns are worktree_path, source_path, change_class, disposition, verdict,
|
|
91
|
+
# target_path, memory_index_line_present, memory_index_line_is_null,
|
|
92
|
+
# memory_index_line, line_ending, host_token_scan_type, host_token_scan_result,
|
|
93
|
+
# host_token_scan_pattern_set_id, evidence.
|
|
94
|
+
#
|
|
95
|
+
# The key-presence and is-null columns exist because the upstream contract
|
|
96
|
+
# distinguishes "key absent, skip the record" from "null is valid and means there is
|
|
97
|
+
# no index line to carry", and the type column exists because a host_token_scan that
|
|
98
|
+
# is present but is not an object must be refused rather than read. Tab is the
|
|
99
|
+
# transport delimiter precisely because memory_index_line may legitimately contain a
|
|
100
|
+
# pipe character.
|
|
101
|
+
#
|
|
102
|
+
# A jq parse failure and either top-level check failing are the same outcome: emit
|
|
103
|
+
# ACTION|preserve-manifest|<manifest-path>|REJECTED, write a diagnostic to stderr, and
|
|
104
|
+
# return 1 with nothing staged.
|
|
105
|
+
#
|
|
106
|
+
# Args: $1 = manifest path. Returns 0 on success, 1 on rejection, 127 when jq cannot
|
|
107
|
+
# be resolved.
|
|
108
|
+
local manifest=${1-}
|
|
109
|
+
local jq_bin out="" rc=0
|
|
110
|
+
jq_bin=$(preserve_resolve_jq) || return 127
|
|
111
|
+
local filter='
|
|
112
|
+
if (.tool != "cleanup-merged-worktrees") or (.schema_version != 1) then
|
|
113
|
+
error("manifest failed top-level validation")
|
|
114
|
+
else
|
|
115
|
+
(.preserved_files // [])[]
|
|
116
|
+
| [ (.worktree_path // ""), (.source_path // ""), (.change_class // ""),
|
|
117
|
+
(.disposition // ""), (.verdict // ""), (.target_path // ""),
|
|
118
|
+
(has("memory_index_line") | tostring), ((.memory_index_line == null) | tostring),
|
|
119
|
+
(.memory_index_line // ""), (.line_ending // ""), (.host_token_scan | type),
|
|
120
|
+
(if (.host_token_scan | type) == "object" then (.host_token_scan.result // "") else "" end),
|
|
121
|
+
(if (.host_token_scan | type) == "object" then (.host_token_scan.pattern_set_id // "") else "" end),
|
|
122
|
+
(.evidence // "") ]
|
|
123
|
+
| @tsv
|
|
124
|
+
end'
|
|
125
|
+
out=$("$jq_bin" -r "$filter" "$manifest") || rc=$?
|
|
126
|
+
if ((rc != 0)); then
|
|
127
|
+
printf 'ACTION|preserve-manifest|%s|REJECTED\n' "$manifest"
|
|
128
|
+
printf 'cleanup-worktrees: manifest rejected (jq rc=%s): %s\n' "$rc" "$manifest" >&2
|
|
129
|
+
return 1
|
|
130
|
+
fi
|
|
131
|
+
if [[ -n $out ]]; then
|
|
132
|
+
printf '%s\n' "$out"
|
|
133
|
+
fi
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
preserve_report_invalid() {
|
|
137
|
+
# Report one record-validation violation and return 1.
|
|
138
|
+
#
|
|
139
|
+
# The result record travels on the existing ACTION| result channel so this library
|
|
140
|
+
# introduces no second outcome vocabulary. The human-readable reason naming the
|
|
141
|
+
# offending field goes to stderr, keeping the machine-parseable stream on stdout free
|
|
142
|
+
# of prose.
|
|
143
|
+
#
|
|
144
|
+
# Args: $1 = target_path, which is legitimately empty for a record whose target_path
|
|
145
|
+
# is itself the invalid field; $2 = the reason.
|
|
146
|
+
printf 'ACTION|preserve-stage|%s|SKIPPED-INVALID\n' "${1-}"
|
|
147
|
+
printf 'cleanup-worktrees: preserve record skipped (%s)\n' "${2-}" >&2
|
|
148
|
+
return 1
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
preserve_relative_path_reason() {
|
|
152
|
+
# Echo the reason the named value is not an acceptable repo-relative path, or nothing
|
|
153
|
+
# at all when it is acceptable. Both the drive-letter and the leading-slash absolute
|
|
154
|
+
# forms are rejected, because the manifest is authored on a Windows host and its
|
|
155
|
+
# upstream example encodes worktree paths in drive-letter form.
|
|
156
|
+
#
|
|
157
|
+
# Args: $1 = field name, $2 = value.
|
|
158
|
+
local name=${1-} val=${2-}
|
|
159
|
+
if [[ -z $val ]]; then
|
|
160
|
+
printf '%s is empty' "$name"
|
|
161
|
+
elif [[ $val == /* || $val == [A-Za-z]:[\\/]* ]]; then
|
|
162
|
+
printf '%s is absolute: %s' "$name" "$val"
|
|
163
|
+
elif [[ $val == ".." || $val == "../"* || $val == *"/../"* || $val == *"/.." ]]; then
|
|
164
|
+
printf '%s contains a .. segment: %s' "$name" "$val"
|
|
165
|
+
fi
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
preserve_validate_record() {
|
|
169
|
+
# Validate one tab-separated manifest record fail-closed against the D4 field matrix.
|
|
170
|
+
#
|
|
171
|
+
# Every field is validated at consumption because the manifest is written by the skill's
|
|
172
|
+
# editorial pass rather than by a script, and nothing upstream validates the
|
|
173
|
+
# preserved_files[] array at all. Each violation skips the record and reports
|
|
174
|
+
# ACTION|preserve-stage|<target-path>|SKIPPED-INVALID; the first violation in the chain
|
|
175
|
+
# below wins, because one bad field skips the record whatever the others say.
|
|
176
|
+
#
|
|
177
|
+
# change_class is validated for vocabulary but does NOT change the staging action: an
|
|
178
|
+
# untracked and a modified record are both copied to target_path and staged, stated
|
|
179
|
+
# here so no later change invents a divergent path for one of them.
|
|
180
|
+
#
|
|
181
|
+
# host_token_scan is refused when it is absent, is not a JSON object, is missing either
|
|
182
|
+
# member, or carries a result outside clean/tokens_present: a record whose scan metadata
|
|
183
|
+
# cannot be read is one whose provenance is unknown, and staging it would defeat the
|
|
184
|
+
# pre-pass. line_ending is deliberately NOT a skip condition, because it is advisory, it
|
|
185
|
+
# describes the target rather than the record, and it is re-derived on the write path.
|
|
186
|
+
#
|
|
187
|
+
# Side effect, relied on by preserve_plan: the split fields are left in the global
|
|
188
|
+
# array PRESERVE_FIELDS, so the caller reads the columns without splitting twice.
|
|
189
|
+
#
|
|
190
|
+
# Args: $1 = the tab-separated record. Returns 0 when the record is valid, 1 when it
|
|
191
|
+
# was skipped and reported.
|
|
192
|
+
preserve_split_tsv "${1-}"
|
|
193
|
+
local wt=${PRESERVE_FIELDS[0]-} src=${PRESERVE_FIELDS[1]-} cls=${PRESERVE_FIELDS[2]-}
|
|
194
|
+
local disp=${PRESERVE_FIELDS[3]-} verdict=${PRESERVE_FIELDS[4]-} tgt=${PRESERVE_FIELDS[5]-}
|
|
195
|
+
local present=${PRESERVE_FIELDS[6]-} isnull=${PRESERVE_FIELDS[7]-}
|
|
196
|
+
local scantype=${PRESERVE_FIELDS[10]-} scanresult=${PRESERVE_FIELDS[11]-}
|
|
197
|
+
local scanpsid=${PRESERVE_FIELDS[12]-} ev=${PRESERVE_FIELDS[13]-}
|
|
198
|
+
local verdicts=" DEAD_ONE_OFF ALREADY_SOLVED_ELSEWHERE STALE_OR_CONTRADICTED GENUINELY_NEW STILL_RELEVANT "
|
|
199
|
+
local reason="" psrc ptgt
|
|
200
|
+
psrc=$(preserve_relative_path_reason source_path "$src")
|
|
201
|
+
ptgt=$(preserve_relative_path_reason target_path "$tgt")
|
|
202
|
+
if ((${#PRESERVE_FIELDS[@]} != 14)); then
|
|
203
|
+
reason="record carries ${#PRESERVE_FIELDS[@]} columns, expected 14"
|
|
204
|
+
elif [[ -z $wt ]]; then
|
|
205
|
+
reason="worktree_path is empty"
|
|
206
|
+
elif [[ -n $psrc ]]; then
|
|
207
|
+
reason=$psrc
|
|
208
|
+
elif [[ -n $ptgt ]]; then
|
|
209
|
+
reason=$ptgt
|
|
210
|
+
elif [[ $cls != "untracked" && $cls != "modified" ]]; then
|
|
211
|
+
reason="change_class out of vocabulary: $cls"
|
|
212
|
+
elif [[ $disp != "PRESERVE" ]]; then
|
|
213
|
+
reason="disposition is not PRESERVE: $disp"
|
|
214
|
+
elif [[ $verdicts != *" $verdict "* ]]; then
|
|
215
|
+
reason="verdict out of vocabulary: $verdict"
|
|
216
|
+
elif [[ $present != "true" ]]; then
|
|
217
|
+
reason="memory_index_line key is absent"
|
|
218
|
+
elif [[ $isnull != "true" && $isnull != "false" ]]; then
|
|
219
|
+
reason="memory_index_line is neither a string nor null"
|
|
220
|
+
elif [[ $scantype != "object" ]]; then
|
|
221
|
+
reason="host_token_scan is $scantype, expected an object"
|
|
222
|
+
elif [[ -z $scanresult ]]; then
|
|
223
|
+
reason="host_token_scan.result is absent"
|
|
224
|
+
elif [[ $scanresult != "clean" && $scanresult != "tokens_present" ]]; then
|
|
225
|
+
reason="host_token_scan.result out of vocabulary: $scanresult"
|
|
226
|
+
elif [[ -z $scanpsid ]]; then
|
|
227
|
+
reason="host_token_scan.pattern_set_id is absent"
|
|
228
|
+
elif [[ -z $ev ]]; then
|
|
229
|
+
reason="evidence is empty"
|
|
230
|
+
fi
|
|
231
|
+
if [[ -n $reason ]]; then
|
|
232
|
+
preserve_report_invalid "$tgt" "$reason"
|
|
233
|
+
return 1
|
|
234
|
+
fi
|
|
235
|
+
return 0
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
preserve_plan_one() {
|
|
239
|
+
# Plan one manifest record. Performs no write of any kind.
|
|
240
|
+
#
|
|
241
|
+
# The PRESERVE| report record is emitted for every record that survives validation,
|
|
242
|
+
# before any per-record outcome is known, because that record identifies the finding at
|
|
243
|
+
# its origin exactly as DIRTY|<worktree-path>|<status-line> does. The destination
|
|
244
|
+
# outcome travels separately on the ACTION| result channel.
|
|
245
|
+
#
|
|
246
|
+
# The source file is located by plain file I/O at <worktree_path>/<source_path> and is
|
|
247
|
+
# never read through git: the repository-root .claude/agent-memory tree is gitignored,
|
|
248
|
+
# so a git-mediated read would be refused or would need a force flag.
|
|
249
|
+
#
|
|
250
|
+
# Args: $1 = the tab-separated record. Returns 0 when the record was planned and 1 when
|
|
251
|
+
# it was skipped, refused, or reported.
|
|
252
|
+
preserve_validate_record "${1-}" || return 1
|
|
253
|
+
local wt=${PRESERVE_FIELDS[0]} src=${PRESERVE_FIELDS[1]} verdict=${PRESERVE_FIELDS[4]}
|
|
254
|
+
local tgt=${PRESERVE_FIELDS[5]}
|
|
255
|
+
printf 'PRESERVE|%s|%s|%s\n' "$wt" "$src" "$verdict"
|
|
256
|
+
local srcfile="$wt/$src"
|
|
257
|
+
if [[ ! -f $srcfile ]]; then
|
|
258
|
+
# Skip and report, contributing to exit 1. A missing source is never a hard stop:
|
|
259
|
+
# the operator can still stage everything else and re-run once the file is found.
|
|
260
|
+
printf 'ACTION|preserve-stage|%s|MISSING-SOURCE\n' "$tgt"
|
|
261
|
+
printf 'cleanup-worktrees: source file not found in the named worktree: %s\n' "$srcfile" >&2
|
|
262
|
+
return 1
|
|
263
|
+
fi
|
|
264
|
+
# The pattern-set identifier is advisory. A value this library does not own is reported
|
|
265
|
+
# and changes nothing else, including the exit code: the local scan governs regardless.
|
|
266
|
+
if [[ ${PRESERVE_FIELDS[12]} != "cleanup-wt-host-tokens-v1" ]]; then
|
|
267
|
+
printf 'ACTION|preserve-scan|%s|PATTERN-SET-MISMATCH\n' "$tgt"
|
|
268
|
+
fi
|
|
269
|
+
# The host-token pre-pass. It reads only the source file's bytes and the index line's
|
|
270
|
+
# bytes, and it runs for EVERY valid record before the writing phase is entered for any
|
|
271
|
+
# of them, because a match anywhere refuses the whole pass rather than one record.
|
|
272
|
+
#
|
|
273
|
+
# An upstream result of tokens_present short-circuits to a refusal without scanning, as
|
|
274
|
+
# a cheap fail-closed gate. An upstream result of `clean` does NOT skip the local scan:
|
|
275
|
+
# the manifest is written by an editorial pass rather than by a script, so a `clean`
|
|
276
|
+
# claim is re-derived rather than believed.
|
|
277
|
+
if [[ ${PRESERVE_FIELDS[11]} == "tokens_present" ]]; then
|
|
278
|
+
printf 'ACTION|preserve-stage|%s|HOST-TOKEN-BLOCKED\n' "$tgt"
|
|
279
|
+
printf 'cleanup-worktrees: the manifest reports tokens_present for %s\n' "$srcfile" >&2
|
|
280
|
+
PRESERVE_BLOCKED=1
|
|
281
|
+
return 1
|
|
282
|
+
fi
|
|
283
|
+
if ! preserve_scan_host_tokens "$srcfile" "${PRESERVE_FIELDS[8]}"; then
|
|
284
|
+
printf 'ACTION|preserve-stage|%s|HOST-TOKEN-BLOCKED\n' "$tgt"
|
|
285
|
+
PRESERVE_BLOCKED=1
|
|
286
|
+
return 1
|
|
287
|
+
fi
|
|
288
|
+
if cleanup_wt_git -C "$PRESERVE_CWT" check-ignore -q -- "$tgt"; then
|
|
289
|
+
# Refuse without forcing. Forcing an ignored path would produce a commit that
|
|
290
|
+
# appears to preserve the lesson while push-down never distributes it, because
|
|
291
|
+
# only the tracked bundle mirror is ever distributed. No force flag is passed to
|
|
292
|
+
# the staging call under any condition.
|
|
293
|
+
printf 'ACTION|preserve-stage|%s|IGNORED-TARGET\n' "$tgt"
|
|
294
|
+
printf 'cleanup-worktrees: destination is ignored, refusing to force: %s\n' "$tgt" >&2
|
|
295
|
+
return 1
|
|
296
|
+
fi
|
|
297
|
+
# The index decision, including the line-ending re-derivation it rests on, lives in
|
|
298
|
+
# the line-ending library. A memory_index_line of JSON null means the file is not a
|
|
299
|
+
# memory entry: it is copied and staged, and no index is read, created, or appended to.
|
|
300
|
+
local iaction="none" ipath="" iline="" iterm="lf" ipre="no"
|
|
301
|
+
if [[ ${PRESERVE_FIELDS[7]} == "false" ]]; then
|
|
302
|
+
iline=${PRESERVE_FIELDS[8]}
|
|
303
|
+
preserve_plan_index "$tgt" "$iline" "${PRESERVE_FIELDS[9]}" "$PRESERVE_CWT" || return 1
|
|
304
|
+
iaction=$PRESERVE_INDEX_ACTION
|
|
305
|
+
ipath=$PRESERVE_INDEX_PATH
|
|
306
|
+
iterm=$PRESERVE_INDEX_TERM
|
|
307
|
+
ipre=$PRESERVE_INDEX_PRE
|
|
308
|
+
fi
|
|
309
|
+
local entry
|
|
310
|
+
printf -v entry '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s' \
|
|
311
|
+
"$tgt" "$srcfile" "$PRESERVE_CWT/$tgt" "$iaction" "$ipath" "$iline" "$iterm" "$ipre"
|
|
312
|
+
PRESERVE_PLAN_STREAM+=("$entry")
|
|
313
|
+
return 0
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
preserve_plan() {
|
|
317
|
+
# Phase 1 of the two-phase driver: read, validate, decide, and report. It performs no
|
|
318
|
+
# write of any kind, so a failure anywhere in it leaves the consolidation worktree
|
|
319
|
+
# byte-unchanged and there is nothing to undo.
|
|
320
|
+
#
|
|
321
|
+
# Records are processed in LC_ALL=C order of the pair (worktree_path, source_path)
|
|
322
|
+
# rather than in manifest array order, because the report stream's ordering is
|
|
323
|
+
# normative and the manifest is authored by an editorial pass whose array order is not
|
|
324
|
+
# a contract. The sort is field-aware rather than whole-line, so a worktree_path that
|
|
325
|
+
# is a prefix of another cannot reorder the pair.
|
|
326
|
+
#
|
|
327
|
+
# The decisions are accumulated on two globals rather than returned, because a record's
|
|
328
|
+
# outcome has to survive the loop: PRESERVE_PLAN_STREAM carries one tab-separated entry
|
|
329
|
+
# per record that reached the writing phase, and PRESERVE_EXIT accumulates the
|
|
330
|
+
# per-record skip signal that becomes the pass's exit code.
|
|
331
|
+
#
|
|
332
|
+
# Returns 0 when the pass may proceed to the writing phase, 1 when the manifest itself
|
|
333
|
+
# was rejected, 3 on the host-token hard stop, and 127 when a required tool could not
|
|
334
|
+
# be resolved. Per-record skips do NOT change this return value; they accumulate in
|
|
335
|
+
# PRESERVE_EXIT and are surfaced by run_preserve.
|
|
336
|
+
PRESERVE_PLAN_STREAM=()
|
|
337
|
+
PRESERVE_EXIT=0
|
|
338
|
+
PRESERVE_BLOCKED=0
|
|
339
|
+
PRESERVE_CWT=""
|
|
340
|
+
local manifest records sorted line rc=0
|
|
341
|
+
manifest=${CLEANUP_WT_MANIFEST_PATH:-artifacts/orchestration/cleanup-worktrees-manifest.json}
|
|
342
|
+
PRESERVE_CWT=$(consolidation_worktree_path) || return $?
|
|
343
|
+
records=$(preserve_read_manifest "$manifest") || rc=$?
|
|
344
|
+
if ((rc != 0)); then
|
|
345
|
+
# preserve_read_manifest writes its REJECTED record to stdout, which the capture
|
|
346
|
+
# above swallowed, so it is re-emitted here rather than lost.
|
|
347
|
+
[[ -z $records ]] || printf '%s\n' "$records"
|
|
348
|
+
return "$rc"
|
|
349
|
+
fi
|
|
350
|
+
[[ -n $records ]] || return 0
|
|
351
|
+
sorted=$(printf '%s\n' "$records" | LC_ALL=C sort -t $'\t' -k1,1 -k2,2)
|
|
352
|
+
while IFS= read -r line; do
|
|
353
|
+
[[ -n $line ]] || continue
|
|
354
|
+
preserve_plan_one "$line" || PRESERVE_EXIT=1
|
|
355
|
+
done <<<"$sorted"
|
|
356
|
+
# The hard stop is evaluated only after every record has been examined, so the pass
|
|
357
|
+
# reports every match rather than only the first, and the writing phase is not entered
|
|
358
|
+
# for any record. Nothing has been written at this point, so there is nothing to undo.
|
|
359
|
+
((PRESERVE_BLOCKED == 0)) || return 3
|
|
360
|
+
return 0
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
preserve_commit_plan() {
|
|
364
|
+
# Phase 2: the only function in this library that writes anything.
|
|
365
|
+
#
|
|
366
|
+
# It consumes the plan stream phase 1 built and performs exactly four kinds of
|
|
367
|
+
# operation: mkdir -p of the destination directory, a verbatim byte copy of the source,
|
|
368
|
+
# the destination index append or creation, and the staging call. Every decision was
|
|
369
|
+
# already taken in phase 1, so this function contains no policy and is thin enough to
|
|
370
|
+
# leave in the coverage denominator rather than exclude from measurement.
|
|
371
|
+
#
|
|
372
|
+
# The copy is verbatim. No line-ending transformation is applied to the preserved
|
|
373
|
+
# file's own content: transforming it would alter content the editorial pass judged
|
|
374
|
+
# worth keeping, and the staging call re-normalizes tracked text anyway. The
|
|
375
|
+
# line-ending obligation is scoped to the index append, where the defect occurred.
|
|
376
|
+
#
|
|
377
|
+
# A non-zero result from any of the three operations emits
|
|
378
|
+
# ACTION|preserve-stage|<target-path>|FAILED and contributes to exit 1; it never aborts
|
|
379
|
+
# the remaining records, because each record's write is independent of the others.
|
|
380
|
+
local entry tgt srcfile dstfile iaction ipath iline iterm ipre fail
|
|
381
|
+
((${#PRESERVE_PLAN_STREAM[@]} > 0)) || return 0
|
|
382
|
+
for entry in "${PRESERVE_PLAN_STREAM[@]}"; do
|
|
383
|
+
preserve_split_tsv "$entry"
|
|
384
|
+
tgt=${PRESERVE_FIELDS[0]}
|
|
385
|
+
srcfile=${PRESERVE_FIELDS[1]}
|
|
386
|
+
dstfile=${PRESERVE_FIELDS[2]}
|
|
387
|
+
iaction=${PRESERVE_FIELDS[3]}
|
|
388
|
+
ipath=${PRESERVE_FIELDS[4]}
|
|
389
|
+
iline=${PRESERVE_FIELDS[5]}
|
|
390
|
+
iterm=${PRESERVE_FIELDS[6]}
|
|
391
|
+
ipre=${PRESERVE_FIELDS[7]}
|
|
392
|
+
fail=""
|
|
393
|
+
if ! mkdir -p -- "${dstfile%/*}" 2>/dev/null; then
|
|
394
|
+
fail="cannot create the destination directory"
|
|
395
|
+
elif ! cat -- "$srcfile" >"$dstfile" 2>/dev/null; then
|
|
396
|
+
fail="the verbatim byte copy failed"
|
|
397
|
+
elif [[ $iaction != "none" ]] &&
|
|
398
|
+
! preserve_render_index_append "$ipath" "$iline" "$iterm" "$ipre"; then
|
|
399
|
+
fail="the index append failed"
|
|
400
|
+
elif ! cleanup_wt_git -C "$PRESERVE_CWT" add -- "$tgt"; then
|
|
401
|
+
fail="the staging call failed"
|
|
402
|
+
elif [[ $iaction != "none" ]] && ! cleanup_wt_git -C "$PRESERVE_CWT" add -- "$ipath"; then
|
|
403
|
+
fail="staging the index failed"
|
|
404
|
+
fi
|
|
405
|
+
if [[ -n $fail ]]; then
|
|
406
|
+
printf 'ACTION|preserve-stage|%s|FAILED\n' "$tgt"
|
|
407
|
+
printf 'cleanup-worktrees: %s: %s\n' "$fail" "$tgt" >&2
|
|
408
|
+
PRESERVE_EXIT=1
|
|
409
|
+
continue
|
|
410
|
+
fi
|
|
411
|
+
printf 'ACTION|preserve-stage|%s|OK\n' "$tgt"
|
|
412
|
+
done
|
|
413
|
+
return 0
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
run_preserve() {
|
|
417
|
+
# Drive the preserve pass and return the exit code the arm's contract defines:
|
|
418
|
+
# 0 every valid record staged, nothing skipped, no index created, no token match
|
|
419
|
+
# 1 at least one record skipped, refused, or failed, or an index was created
|
|
420
|
+
# 3 the host-token hard stop; the writing phase was not entered and nothing staged
|
|
421
|
+
# 127 a required tool could not be resolved; nothing staged
|
|
422
|
+
#
|
|
423
|
+
# Two preconditions are checked before any record is read, and both fail closed. The
|
|
424
|
+
# arm never creates the consolidation worktree: creating it would hide an operator
|
|
425
|
+
# mistake behind a directory that looks correct and holds nothing.
|
|
426
|
+
local cwt manifest rc=0
|
|
427
|
+
manifest=${CLEANUP_WT_MANIFEST_PATH:-artifacts/orchestration/cleanup-worktrees-manifest.json}
|
|
428
|
+
cwt=$(consolidation_worktree_path) || return $?
|
|
429
|
+
if [[ ! -d $cwt ]]; then
|
|
430
|
+
printf 'ACTION|preserve-stage||MISSING-WORKTREE\n'
|
|
431
|
+
printf 'cleanup-worktrees: consolidation worktree does not exist: %s\n' "$cwt" >&2
|
|
432
|
+
return 1
|
|
433
|
+
fi
|
|
434
|
+
if [[ ! -f $manifest || ! -r $manifest ]]; then
|
|
435
|
+
printf 'ACTION|preserve-manifest|%s|MISSING\n' "$manifest"
|
|
436
|
+
printf 'cleanup-worktrees: manifest is not an existing readable file: %s\n' "$manifest" >&2
|
|
437
|
+
return 1
|
|
438
|
+
fi
|
|
439
|
+
preserve_plan || rc=$?
|
|
440
|
+
if ((rc != 0)); then
|
|
441
|
+
return "$rc"
|
|
442
|
+
fi
|
|
443
|
+
preserve_commit_plan || rc=$?
|
|
444
|
+
if ((rc != 0)); then
|
|
445
|
+
return "$rc"
|
|
446
|
+
fi
|
|
447
|
+
return "$PRESERVE_EXIT"
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
preserve_scan_host_tokens() {
|
|
451
|
+
# Scan the incoming bytes for host-identifying tokens, implementing the pattern set
|
|
452
|
+
# whose identifier this library owns: cleanup-wt-host-tokens-v1.
|
|
453
|
+
#
|
|
454
|
+
# Scope, narrow on purpose. Exactly two things are read: the bytes of the file named by
|
|
455
|
+
# the (worktree_path, source_path) pair, and the bytes of the memory_index_line string
|
|
456
|
+
# that will be appended. The manifest is NOT scanned, because its own worktree_path
|
|
457
|
+
# values legitimately carry the very tokens refused in staged content; the destination
|
|
458
|
+
# index, the consolidation branch, the repository tree, the push-down bundle, the
|
|
459
|
+
# environment, and the worktree path are excluded for the same reason, since a
|
|
460
|
+
# repository-wide scan would flag tracked files that must contain those strings.
|
|
461
|
+
#
|
|
462
|
+
# Matching is POSIX extended regular expression under LC_ALL=C, case-insensitive, with
|
|
463
|
+
# the input treated as text, evaluated one pattern at a time in ascending identifier
|
|
464
|
+
# order so the first match can be named in the diagnostic.
|
|
465
|
+
#
|
|
466
|
+
# Args: $1 = the source file path, $2 = the index line. Returns 0 when clean and 1 on a
|
|
467
|
+
# match, with the matched identifier written to stderr.
|
|
468
|
+
local srcfile=${1-} iline=${2-}
|
|
469
|
+
local -a ids=(HT1 HT2 HT3 HT4 HT5 HT6)
|
|
470
|
+
local -a patterns=(
|
|
471
|
+
$'[A-Za-z]:[\\\\/]+users[\\\\/]+[^\\\\/[:space:]"\']+'
|
|
472
|
+
$'/mnt/[a-z]/users/[^/[:space:]"\']+'
|
|
473
|
+
'/home/[a-z][a-z0-9._-]*'
|
|
474
|
+
$'[\\\\/][a-z0-9]{1,6}~[0-9][\\\\/]'
|
|
475
|
+
'[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}'
|
|
476
|
+
'%(userprofile|username|computername|homepath)%'
|
|
477
|
+
)
|
|
478
|
+
local i id pattern
|
|
479
|
+
for i in "${!ids[@]}"; do
|
|
480
|
+
id=${ids[$i]}
|
|
481
|
+
pattern=${patterns[$i]}
|
|
482
|
+
if printf '%s' "$iline" | LC_ALL=C grep -a -i -E -q -e "$pattern"; then
|
|
483
|
+
printf 'cleanup-worktrees: host token matched (%s) in the index line\n' "$id" >&2
|
|
484
|
+
return 1
|
|
485
|
+
fi
|
|
486
|
+
if [[ -f $srcfile ]] && LC_ALL=C grep -a -i -E -q -e "$pattern" -- "$srcfile"; then
|
|
487
|
+
printf 'cleanup-worktrees: host token matched (%s) in %s\n' "$id" "$srcfile" >&2
|
|
488
|
+
return 1
|
|
489
|
+
fi
|
|
490
|
+
done
|
|
491
|
+
return 0
|
|
492
|
+
}
|