@ikon85/agent-workflow-kit 0.37.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/kit-update/SKILL.md +33 -1
- package/.agents/skills/orchestrate-wave/SKILL.md +5 -5
- package/.agents/skills/setup-workflow/SKILL.md +42 -4
- package/.agents/skills/setup-workflow/board-sync.md +6 -2
- package/.agents/skills/setup-workflow/workflow-advisories.md +34 -0
- package/.agents/skills/setup-workflow/worktree-lifecycle.md +47 -1
- package/.agents/skills/wrapup/SKILL.md +46 -0
- package/.claude/hooks/drift-guard.py +212 -21
- package/.claude/skills/kit-update/SKILL.md +33 -1
- package/.claude/skills/orchestrate-wave/SKILL.md +5 -5
- package/.claude/skills/setup-workflow/SKILL.md +42 -4
- package/.claude/skills/setup-workflow/board-sync.md +6 -2
- package/.claude/skills/setup-workflow/workflow-advisories.md +34 -0
- package/.claude/skills/setup-workflow/worktree-lifecycle.md +47 -1
- package/.claude/skills/wrapup/SKILL.md +46 -0
- package/README.md +73 -0
- package/agent-workflow-kit.package.json +49 -25
- package/docs/adr/0007-session-teardown-requires-provenance-bound-ownership.md +88 -0
- package/docs/agents/workflow-capabilities.json +11 -1
- package/package.json +1 -1
- package/scripts/board_bootstrap.py +367 -0
- package/scripts/kit-update-pr.mjs +20 -7
- package/scripts/kit-update-pr.test.mjs +29 -0
- package/scripts/profile_globs.py +347 -0
- package/scripts/test_board_bootstrap.py +348 -0
- package/scripts/test_drift_guard_diagnostics.py +295 -0
- package/scripts/test_orchestrate_wave_contract.py +9 -0
- package/scripts/test_profile_globs.py +280 -0
- package/scripts/test_skill_setup_workflow_seeds.py +87 -0
- package/scripts/test_worktree_wrapup_contract.py +1592 -3
- package/scripts/workflow-advisories/core.py +29 -4
- package/scripts/worktree-lifecycle/README.md +139 -0
- package/scripts/worktree-lifecycle/capabilities.json +9 -1
- package/scripts/worktree-lifecycle/cleanup.py +22 -51
- package/scripts/worktree-lifecycle/core.py +1206 -5
- package/scripts/worktree-lifecycle/profile.py +38 -3
- package/scripts/worktree-lifecycle/session.py +1857 -0
- package/scripts/worktree-lifecycle/setup.py +15 -0
- package/scripts/wrapup-land.py +461 -14
- package/src/cli.mjs +32 -1
- package/src/commands/update.mjs +30 -21
- package/src/consumer-migrations.json +19 -0
- package/src/lib/bundle.mjs +11 -0
- package/src/lib/consumerMigrations.mjs +161 -0
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
"""Profile-driven decision core for non-blocking Workflow Advisories."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import importlib.util
|
|
5
5
|
import json
|
|
6
6
|
import re
|
|
7
7
|
import subprocess
|
|
8
|
+
import sys
|
|
8
9
|
from dataclasses import dataclass
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
12
|
+
PROFILE_GLOBS_MODULE = "_agent_workflow_kit_profile_globs"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_profile_globs():
|
|
16
|
+
"""Load the one shared repository-relative glob dialect exactly once."""
|
|
17
|
+
module = sys.modules.get(PROFILE_GLOBS_MODULE)
|
|
18
|
+
if module is not None:
|
|
19
|
+
return module
|
|
20
|
+
path = Path(__file__).resolve().parents[1] / "profile_globs.py"
|
|
21
|
+
spec = importlib.util.spec_from_file_location(PROFILE_GLOBS_MODULE, path)
|
|
22
|
+
if spec is None or spec.loader is None:
|
|
23
|
+
raise ImportError(f"cannot load the shared profile glob dialect from {path}")
|
|
24
|
+
module = importlib.util.module_from_spec(spec)
|
|
25
|
+
sys.modules[PROFILE_GLOBS_MODULE] = module
|
|
26
|
+
spec.loader.exec_module(module)
|
|
27
|
+
return module
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Consumer profile globs are matched here exactly as Worktree Lifecycle matches
|
|
31
|
+
# its own: one dialect, so an advisory and a deletion decision can never
|
|
32
|
+
# disagree about which repository-relative paths a pattern selects.
|
|
33
|
+
path_glob_matches = load_profile_globs().path_glob_matches
|
|
34
|
+
|
|
11
35
|
|
|
12
36
|
@dataclass(frozen=True)
|
|
13
37
|
class Decision:
|
|
@@ -87,7 +111,8 @@ def baseline_decision(profile: dict, payload: dict, root: Path, branch: str) ->
|
|
|
87
111
|
raw_path = payload.get("tool_input", {}).get("file_path")
|
|
88
112
|
relative = _repo_relative(root, raw_path) if raw_path else None
|
|
89
113
|
if not relative or not any(
|
|
90
|
-
|
|
114
|
+
path_glob_matches(relative, pattern)
|
|
115
|
+
for pattern in config.get("sourceGlobs", [])
|
|
91
116
|
):
|
|
92
117
|
return Decision(None, "PreToolUse")
|
|
93
118
|
manifest = root / config.get("manifestPath", ".agent/baseline.json")
|
|
@@ -119,7 +144,7 @@ def pre_refactor_decision(profile: dict, payload: dict, root: Path) -> Decision:
|
|
|
119
144
|
seen: set[tuple[str, ...]] = set()
|
|
120
145
|
for surface in config.get("surfaces", []):
|
|
121
146
|
if not any(
|
|
122
|
-
|
|
147
|
+
path_glob_matches(path, pattern)
|
|
123
148
|
for path in changed
|
|
124
149
|
for pattern in surface.get("globs", [])
|
|
125
150
|
):
|
|
@@ -170,7 +195,7 @@ def stop_check_decision(profile: dict, payload: dict, root: Path) -> Decision:
|
|
|
170
195
|
seen: set[tuple[str, ...]] = set()
|
|
171
196
|
for surface in config.get("surfaces", []):
|
|
172
197
|
if not any(
|
|
173
|
-
|
|
198
|
+
path_glob_matches(path, pattern)
|
|
174
199
|
for path in changed
|
|
175
200
|
for pattern in surface.get("globs", [])
|
|
176
201
|
):
|
|
@@ -19,6 +19,29 @@ they do not carry a second branch regex, worktree traversal, or failure policy.
|
|
|
19
19
|
- `riskyCommandPatterns`: commands that must target the active linked worktree.
|
|
20
20
|
- `scratchPatterns`: consumer-owned glob patterns for untracked disposable
|
|
21
21
|
planning artefacts. No filename is assumed by Core.
|
|
22
|
+
- `wrapup.landingGeneratedArtifactPatterns`: an explicitly reviewed
|
|
23
|
+
consumer-owned allowlist for outputs created by the landing commands. It is
|
|
24
|
+
deletion authority, so setup never derives it from `.gitignore` alone and
|
|
25
|
+
update never installs a universal default.
|
|
26
|
+
|
|
27
|
+
Profile globs use one repository-relative POSIX dialect, implemented once in
|
|
28
|
+
`scripts/profile_globs.py` and loaded by this core and by
|
|
29
|
+
`scripts/workflow-advisories/core.py`. `*` and `?` stay inside one path
|
|
30
|
+
segment; `[seq]`/`[!seq]` are per-segment character classes; `**` as a whole
|
|
31
|
+
segment matches zero or more segments, so a leading `**/` also matches the
|
|
32
|
+
repository root and `dir/**` also matches `dir` itself; matching is always
|
|
33
|
+
case-sensitive on every host filesystem; and a pattern must match the whole
|
|
34
|
+
path. For example, `**/__pycache__/**` matches both root and nested caches,
|
|
35
|
+
while `dist-kit/*` does not match `dist-kit/a/b`.
|
|
36
|
+
|
|
37
|
+
Because both capabilities load the same matcher, a pattern can never select one
|
|
38
|
+
set of paths for an advisory and a different set for a deletion decision. Run
|
|
39
|
+
`python3 scripts/profile_globs.py <profile.json>` to review an installed
|
|
40
|
+
profile: it names every pattern whose match set narrows or widens against that
|
|
41
|
+
key's legacy matcher, prints the witness path proving it, marks the keys that
|
|
42
|
+
carry deletion authority, and exits 1 when anything needs review. It reads the
|
|
43
|
+
profile and never rewrites a pattern, so a migration cannot silently expand
|
|
44
|
+
cleanup authority.
|
|
22
45
|
|
|
23
46
|
Unknown or malformed events fail open without changing repository state.
|
|
24
47
|
Security-sensitive, profile-matched edits and commands fail closed only when
|
|
@@ -49,11 +72,127 @@ same removable inventory, deletes only the exact contained regular scratch
|
|
|
49
72
|
files from that inventory, and uses ordinary `git worktree remove`. It never
|
|
50
73
|
bypasses Git's final concurrent-change check with force removal.
|
|
51
74
|
|
|
75
|
+
The generic setup route atomically records its ignored and complete
|
|
76
|
+
untracked-file inventories in the linked worktree's Git metadata after its
|
|
77
|
+
configured setup steps. The record is bound to the worktree path, branch, root
|
|
78
|
+
device/inode, and setup HEAD, and carries a canonical digest. Existing
|
|
79
|
+
worktrees can receive a conservative baseline only when they are the exact
|
|
80
|
+
registered no-follow directory on an attached branch with a clean tracked
|
|
81
|
+
worktree and index. Setup reuse defers that backfill, without blocking the
|
|
82
|
+
existing worktree, while tracked work is dirty or landing-generated blockers
|
|
83
|
+
are present. Landing classifies those blockers before writing any baseline;
|
|
84
|
+
after they are moved, every remaining current ignored and untracked path is
|
|
85
|
+
recorded as pre-existing and therefore protected. A corrupt baseline or an
|
|
86
|
+
active landing attempt is never overwritten. The claim-bound session route
|
|
87
|
+
below captures its stricter baseline before project setup so a failed setup has
|
|
88
|
+
an exact recovery boundary.
|
|
89
|
+
|
|
90
|
+
Before merge, the committed worktree profile may only nominate exact,
|
|
91
|
+
identity-bound landing candidates; it authorizes no deletion. After merge and
|
|
92
|
+
`fetch origin/main`, cleanup reloads the profile directly from canonical
|
|
93
|
+
`origin/main`, requires its scratch and generator policies to equal the
|
|
94
|
+
worktree candidate and the policy digest frozen at attempt start, and only then
|
|
95
|
+
authorizes mutation. Every supplied generator-evidence path is independently
|
|
96
|
+
checked against that canonical generator policy. A missing policy is distinct
|
|
97
|
+
from an explicit empty policy. An unmerged or transient branch policy therefore
|
|
98
|
+
cannot grant itself broader cleanup authority.
|
|
99
|
+
|
|
100
|
+
Canonical policy that drifts between attempt start and post-merge cleanup keeps
|
|
101
|
+
that refusal, and the refusal names its supported recovery. Recovery re-derives
|
|
102
|
+
authority from the merged canonical policy alone: it never consults the stale
|
|
103
|
+
worktree candidate, never re-scans for new candidates, requires the branch to
|
|
104
|
+
already be an ancestor of canonical main, and requires each frozen identity to
|
|
105
|
+
be both named by canonical policy and unchanged on disk. Evidence outside
|
|
106
|
+
canonical policy, a changed identity, and pre-existing or foreign state stop the
|
|
107
|
+
recovery instead of being deleted, and a repeated run after a completed teardown
|
|
108
|
+
is a no-op.
|
|
109
|
+
|
|
110
|
+
The landing adapter may carry exact scratch evidence only for current ignored
|
|
111
|
+
files that match the consumer-owned
|
|
112
|
+
`wrapup.landingGeneratedArtifactPatterns` profile and were absent from that
|
|
113
|
+
creation baseline. Missing, changed, or incoherent provenance stops landing
|
|
114
|
+
cleanup. Initial/profile-matched files, unmatched files, symlinks, and writes
|
|
115
|
+
after the landing evidence snapshot remain cleanup stops; deletion still uses
|
|
116
|
+
the same descriptor-bound regular-file primitive and a second inventory check.
|
|
117
|
+
Mutable session logs belong in explicit `scratchPatterns`, not in the landing
|
|
118
|
+
generator allowlist: their identity is frozen at final cleanup assessment, so
|
|
119
|
+
normal logging before teardown remains live while a later append or replacement
|
|
120
|
+
still stops deletion.
|
|
121
|
+
Landing-start blockers are classified before a journal is written, so moving a
|
|
122
|
+
protected blocker permits a clean next attempt. An explicit relinquish archives
|
|
123
|
+
either a started or frozen attempt, including drifted evidence, without
|
|
124
|
+
deleting or claiming any file; the next preflight treats every current file as
|
|
125
|
+
pre-existing. Exact unchanged frozen evidence remains directly resumable. The
|
|
126
|
+
attempt journal name is classified without following a symlink, so a symlinked
|
|
127
|
+
or dangling journal entry stops instead of being read or replaced, and each
|
|
128
|
+
archived receipt is filed under a contract-version-neutral stem plus its own
|
|
129
|
+
recorded contract version. A journal written under a superseded contract is
|
|
130
|
+
classified as legacy rather than corrupt when it still satisfies its own
|
|
131
|
+
recorded contract; that refusal names the archive route explicitly, while a
|
|
132
|
+
journal that fails its own contract is still reported as incoherent evidence.
|
|
133
|
+
|
|
52
134
|
`cleanup.py sweep` is the read-only inventory entrypoint. It accounts once for
|
|
53
135
|
every linked worktree and local branch, reports issue/PR/merge/age/removal
|
|
54
136
|
facts, and counts merged remote branches separately. It never removes a
|
|
55
137
|
worktree or branch.
|
|
56
138
|
|
|
139
|
+
## Session-owned teardown
|
|
140
|
+
|
|
141
|
+
`session.py` is the narrower orchestration route. It binds one receipt in the
|
|
142
|
+
Git common directory to the active `wave-active/<anchor>` annotated claim:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
python3 scripts/worktree-lifecycle/session.py begin --anchor <n> --owner <run> --base <wave-head>
|
|
146
|
+
python3 scripts/worktree-lifecycle/session.py create --anchor <n> --owner <run> --base <current-wave-head> <issue> <slug> <type>
|
|
147
|
+
python3 scripts/worktree-lifecycle/session.py recover --anchor <n> --owner <run> --branch <exact-branch>
|
|
148
|
+
python3 scripts/worktree-lifecycle/session.py seal --anchor <n> --owner <run>
|
|
149
|
+
python3 scripts/worktree-lifecycle/session.py inspect --anchor <n> --owner <run> --main origin/main
|
|
150
|
+
python3 scripts/worktree-lifecycle/session.py teardown --anchor <n> --owner <run> --main origin/main
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Only `create` can add an inventory row, and it refuses any branch or path that
|
|
154
|
+
already existed. It journals a provisional exact row before `git worktree add`,
|
|
155
|
+
then captures the shared artifact baseline before project setup and promotes
|
|
156
|
+
the row only after setup succeeds. Session inspection
|
|
157
|
+
therefore accepts generated scratch only when the exact path matches the
|
|
158
|
+
profile and is absent from the creation baseline; missing, changed, or
|
|
159
|
+
incoherent provenance stops. `seal` records the final exact branch OIDs.
|
|
160
|
+
Inspection reports ancestry, one-to-one stable patch equivalence, unique
|
|
161
|
+
content, and ambiguity separately. Empty commits, non-ancestry merge commits,
|
|
162
|
+
duplicate patch IDs, open or unreadable PR evidence, dirt, protected names,
|
|
163
|
+
stale registrations, recreated removed targets, and identity drift stop the
|
|
164
|
+
whole teardown before mutation.
|
|
165
|
+
|
|
166
|
+
If setup fails, the provisional row becomes `recovery-pending`. Automatic and
|
|
167
|
+
explicit `recover` use the same bounded route: active claim and receipt,
|
|
168
|
+
protected name, PR state, exact ref OID, registration/root identity, and the
|
|
169
|
+
creation baseline are revalidated. A branch-only remainder is compare-deleted
|
|
170
|
+
only at its provisional OID. A registered worktree whose add succeeded before
|
|
171
|
+
root journaling is adopted only when its Git registration/backlink, branch,
|
|
172
|
+
OID, and exact-clean inventory all match.
|
|
173
|
+
|
|
174
|
+
For failed project setup, exact index/worktree diff hashes and paths are frozen
|
|
175
|
+
without storing diff content. Exact regular-file or no-follow symlink identity
|
|
176
|
+
is frozen for every setup-created untracked path. Recovery revalidates that
|
|
177
|
+
evidence, restores only the frozen tracked paths to the creation OID, and
|
|
178
|
+
unlinks only matching untracked identities; later foreign files,
|
|
179
|
+
modifications, or replacements stop without losing receipt ownership. If
|
|
180
|
+
identity capture cannot finish, the receipt retains only a bounded failure
|
|
181
|
+
class and can retry from the unchanged inventory or an exact-clean worktree.
|
|
182
|
+
Raw command, exception, stdout, and stderr text is never retained or archived.
|
|
183
|
+
Recovery uses ordinary worktree removal and compare-deletes the exact ref.
|
|
184
|
+
|
|
185
|
+
Teardown re-runs that complete assessment, archives every recovery OID in the
|
|
186
|
+
receipt before its first mutation, and revalidates canonical main, the active
|
|
187
|
+
claim and receipt, profile protection, PR state, branch OID, worktree
|
|
188
|
+
registration/root identity, dirt, and the exact scratch inventory immediately
|
|
189
|
+
before each target mutation. After ordinary worktree removal it rechecks the
|
|
190
|
+
ref-side gates, then compare-deletes only the recorded local refs with
|
|
191
|
+
`git update-ref -d <ref> <expected-oid>`. A concurrent ref move therefore
|
|
192
|
+
survives, while a partial run keeps its recovery OIDs and can resume. Generic
|
|
193
|
+
cleanup and sweep never consume this ownership route and never infer authority
|
|
194
|
+
over a foreign branch.
|
|
195
|
+
|
|
57
196
|
Claude hook wiring and any Codex adaptation consume this same profile and core.
|
|
58
197
|
An adapter may change only the surface event envelope; it must preserve the
|
|
59
198
|
core verdict and message.
|
|
@@ -39,7 +39,15 @@
|
|
|
39
39
|
{
|
|
40
40
|
"historicalPath": "scripts/cleanup-worktrees.sh",
|
|
41
41
|
"artifact": "scripts/worktree-lifecycle/cleanup.py",
|
|
42
|
-
"
|
|
42
|
+
"supportingArtifacts": ["scripts/worktree-lifecycle/session.py"],
|
|
43
|
+
"primitives": [
|
|
44
|
+
"profile",
|
|
45
|
+
"git-facts",
|
|
46
|
+
"cleanup-safety",
|
|
47
|
+
"claim-bound-session-receipt",
|
|
48
|
+
"patch-equivalence",
|
|
49
|
+
"compare-and-delete"
|
|
50
|
+
]
|
|
43
51
|
}
|
|
44
52
|
]
|
|
45
53
|
}
|
|
@@ -4,22 +4,22 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import argparse
|
|
7
|
-
from contextlib import contextmanager
|
|
8
7
|
from dataclasses import asdict, replace
|
|
9
8
|
import json
|
|
10
|
-
import os
|
|
11
|
-
import stat
|
|
12
9
|
import sys
|
|
13
|
-
from pathlib import Path
|
|
10
|
+
from pathlib import Path
|
|
14
11
|
|
|
15
12
|
from core import (
|
|
16
13
|
LifecycleError,
|
|
14
|
+
bind_cleanup_scratch_evidence,
|
|
17
15
|
classify_cleanup,
|
|
18
16
|
collect_cleanup_facts,
|
|
19
17
|
collect_sweep,
|
|
20
18
|
load_profile,
|
|
21
19
|
main_worktree,
|
|
20
|
+
remove_authorized_scratch,
|
|
22
21
|
run,
|
|
22
|
+
verified_worktree_root,
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
|
|
@@ -75,52 +75,18 @@ def collect_assessment(profile, main: Path, worktree: Path, gh_command: str):
|
|
|
75
75
|
worktree,
|
|
76
76
|
)
|
|
77
77
|
state = pr_state(gh_command, main, facts.branch)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
@contextmanager
|
|
82
|
-
def verified_worktree_root(root: Path, expected_device: int, expected_inode: int):
|
|
83
|
-
directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
|
|
84
|
-
no_follow = getattr(os, "O_NOFOLLOW", 0)
|
|
85
|
-
descriptor = None
|
|
78
|
+
assessment = classify_cleanup(profile, replace(facts, pr_state=state))
|
|
86
79
|
try:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
except
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def remove_contained_regular(root_descriptor: int, relative: str) -> None:
|
|
100
|
-
path = PurePosixPath(relative)
|
|
101
|
-
if path.is_absolute() or not path.parts or any(part in {"", ".", ".."} for part in path.parts):
|
|
102
|
-
raise LifecycleError(f"unsafe scratch path: {relative}")
|
|
103
|
-
directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
|
|
104
|
-
no_follow = getattr(os, "O_NOFOLLOW", 0)
|
|
105
|
-
descriptors = []
|
|
106
|
-
try:
|
|
107
|
-
current = root_descriptor
|
|
108
|
-
for component in path.parts[:-1]:
|
|
109
|
-
current = os.open(
|
|
110
|
-
component,
|
|
111
|
-
directory_flags | no_follow,
|
|
112
|
-
dir_fd=current,
|
|
113
|
-
)
|
|
114
|
-
descriptors.append(current)
|
|
115
|
-
metadata = os.stat(path.name, dir_fd=current, follow_symlinks=False)
|
|
116
|
-
if not stat.S_ISREG(metadata.st_mode):
|
|
117
|
-
raise LifecycleError(f"scratch path is not a regular file: {relative}")
|
|
118
|
-
os.unlink(path.name, dir_fd=current)
|
|
119
|
-
except OSError as error:
|
|
120
|
-
raise LifecycleError(f"scratch path changed before removal: {relative}") from error
|
|
121
|
-
finally:
|
|
122
|
-
for descriptor in reversed(descriptors):
|
|
123
|
-
os.close(descriptor)
|
|
80
|
+
return bind_cleanup_scratch_evidence(
|
|
81
|
+
profile,
|
|
82
|
+
assessment,
|
|
83
|
+
require_generator_evidence=True,
|
|
84
|
+
)
|
|
85
|
+
except LifecycleError as error:
|
|
86
|
+
return replace(
|
|
87
|
+
assessment,
|
|
88
|
+
reasons=assessment.reasons + (f"scratch evidence stop: {error}",),
|
|
89
|
+
)
|
|
124
90
|
|
|
125
91
|
|
|
126
92
|
def execute(args: argparse.Namespace) -> dict:
|
|
@@ -155,6 +121,7 @@ def execute(args: argparse.Namespace) -> dict:
|
|
|
155
121
|
or latest.assumptions != assessment.assumptions
|
|
156
122
|
or latest.root_device != assessment.root_device
|
|
157
123
|
or latest.root_inode != assessment.root_inode
|
|
124
|
+
or latest.scratch_evidence != assessment.scratch_evidence
|
|
158
125
|
):
|
|
159
126
|
raise LifecycleError("cleanup changed before removal: inventory no longer matches preview")
|
|
160
127
|
with verified_worktree_root(
|
|
@@ -162,8 +129,12 @@ def execute(args: argparse.Namespace) -> dict:
|
|
|
162
129
|
latest.root_device,
|
|
163
130
|
latest.root_inode,
|
|
164
131
|
) as root_descriptor:
|
|
165
|
-
|
|
166
|
-
|
|
132
|
+
remove_authorized_scratch(
|
|
133
|
+
profile,
|
|
134
|
+
root_descriptor,
|
|
135
|
+
latest.scratch_files,
|
|
136
|
+
assessment.scratch_evidence,
|
|
137
|
+
)
|
|
167
138
|
run(["git", "worktree", "remove", str(latest.worktree)], cwd=main)
|
|
168
139
|
run(["git", "branch", "-d", assessment.branch], cwd=main)
|
|
169
140
|
report["removed"] = True
|