@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.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +4 -5
  3. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget-route.ps1 +137 -0
  4. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +95 -66
  5. package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-CiGateParser.ps1 +330 -0
  6. package/resources/claude-customizations/.claude/rules/powershell.md +3 -3
  7. package/resources/claude-customizations/.claude/rules/shell.md +8 -7
  8. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +8 -8
  9. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +234 -0
  10. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_actions_lib.sh +451 -0
  11. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_detached_lib.sh +301 -0
  12. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_dirt_lib.sh +495 -0
  13. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +252 -0
  14. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_lib.sh +496 -0
  15. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_eol_lib.sh +212 -0
  16. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +492 -0
  17. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +476 -0
  18. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +182 -0
  19. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +1 -1
  20. package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +3 -4
  21. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +1 -1
  22. package/resources/claude-customizations/.claude/skills/powershell-change-budget-router/SKILL.md +6 -6
  23. package/resources/claude-customizations/pack-manifests/core.json +11 -0
  24. package/resources/claude-customizations/pack-manifests/powershell.json +1 -0
  25. package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +1 -1
  26. package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +1 -2
  27. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +2 -3
  28. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +2 -3
  29. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +2 -3
  30. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +2 -3
  31. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +2 -3
  32. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +2 -3
  33. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  34. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +285 -80
  35. package/resources/customizations/.github/agents/powershell-orchestrator.agent.md +7 -7
  36. package/resources/customizations/.github/agents/powershell-typed-engineer.agent.md +8 -10
  37. package/resources/customizations/.github/prompts/orchestrate-powershell-work.prompt.md +2 -2
  38. package/resources/customizations/.github/skills/powershell-change-budget-router/SKILL.md +3 -3
  39. 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
+ }