@ikon85/agent-workflow-kit 0.36.5 → 0.38.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.
Files changed (46) hide show
  1. package/.agents/skills/orchestrate-wave/SKILL.md +20 -12
  2. package/.agents/skills/setup-workflow/SKILL.md +24 -3
  3. package/.agents/skills/setup-workflow/orchestrate-wave-seed.md +3 -2
  4. package/.agents/skills/setup-workflow/worktree-lifecycle.md +29 -1
  5. package/.agents/skills/wrapup/SKILL.md +42 -9
  6. package/.claude/hooks/migration-snapshot-reminder.py +1 -1
  7. package/.claude/skills/orchestrate-wave/SKILL.md +11 -11
  8. package/.claude/skills/setup-workflow/SKILL.md +24 -3
  9. package/.claude/skills/setup-workflow/orchestrate-wave-seed.md +3 -2
  10. package/.claude/skills/setup-workflow/worktree-lifecycle.md +29 -1
  11. package/.claude/skills/skill-manifest.json +1 -1
  12. package/.claude/skills/wrapup/SKILL.md +33 -10
  13. package/README.md +88 -1
  14. package/agent-workflow-kit.package.json +36 -28
  15. package/docs/adr/0007-session-teardown-requires-provenance-bound-ownership.md +88 -0
  16. package/docs/agents/workflow-capabilities.json +11 -0
  17. package/package.json +1 -1
  18. package/scripts/anchor_table.py +14 -8
  19. package/scripts/project-skill-extension.mjs +21 -2
  20. package/scripts/readiness.mjs +32 -4
  21. package/scripts/release-state.mjs +19 -9
  22. package/scripts/release-state.test.mjs +71 -8
  23. package/scripts/test_anchor_table.py +69 -0
  24. package/scripts/test_board_sync_create_idempotency.py +15 -2
  25. package/scripts/test_board_sync_wave_title.py +14 -2
  26. package/scripts/test_census_backstop.py +33 -0
  27. package/scripts/test_orchestrate_wave_contract.py +44 -0
  28. package/scripts/test_retro_wrapup_contract.py +19 -2
  29. package/scripts/test_worktree_wrapup_contract.py +1004 -3
  30. package/scripts/test_wrapup_land.py +428 -0
  31. package/scripts/workflow-advisories/core.py +44 -2
  32. package/scripts/worktree-lifecycle/README.md +126 -4
  33. package/scripts/worktree-lifecycle/capabilities.json +9 -1
  34. package/scripts/worktree-lifecycle/cleanup.py +143 -5
  35. package/scripts/worktree-lifecycle/core.py +1383 -20
  36. package/scripts/worktree-lifecycle/profile.py +40 -3
  37. package/scripts/worktree-lifecycle/session.py +1857 -0
  38. package/scripts/worktree-lifecycle/setup.py +15 -0
  39. package/scripts/wrapup-land.py +650 -27
  40. package/src/cli.mjs +60 -27
  41. package/src/lib/bundle.mjs +1 -0
  42. package/src/lib/manifest.mjs +173 -3
  43. package/src/lib/projectSkillExtension.mjs +78 -1
  44. package/src/lib/updateCandidate.mjs +3 -1
  45. package/src/lib/updateDecisions.mjs +2 -2
  46. package/src/lib/verifyUpdateCandidateProtocol.mjs +15 -0
@@ -4,49 +4,187 @@
4
4
  from __future__ import annotations
5
5
 
6
6
  import argparse
7
+ from dataclasses import asdict, replace
7
8
  import json
8
9
  import sys
9
10
  from pathlib import Path
10
11
 
11
- from core import LifecycleError, cleanup_assessment, load_profile, main_worktree, run
12
+ from core import (
13
+ LifecycleError,
14
+ bind_cleanup_scratch_evidence,
15
+ classify_cleanup,
16
+ collect_cleanup_facts,
17
+ collect_sweep,
18
+ load_profile,
19
+ main_worktree,
20
+ remove_authorized_scratch,
21
+ run,
22
+ verified_worktree_root,
23
+ )
12
24
 
13
25
 
14
26
  def parse_args() -> argparse.Namespace:
15
27
  parser = argparse.ArgumentParser()
16
28
  parser.add_argument("--profile", default="docs/agents/workflow-capabilities.json")
17
29
  parser.add_argument("--remove", action="store_true")
18
- parser.add_argument("worktree")
30
+ parser.add_argument("--gh-command", default="gh")
31
+ parser.add_argument("worktree", nargs="?")
19
32
  return parser.parse_args()
20
33
 
21
34
 
35
+ def parse_sweep_args(argv: list[str]) -> argparse.Namespace:
36
+ parser = argparse.ArgumentParser(prog="cleanup.py sweep")
37
+ parser.add_argument("--profile", default="docs/agents/workflow-capabilities.json")
38
+ parser.add_argument("--gh-command", default="gh")
39
+ return parser.parse_args(argv)
40
+
41
+
42
+ def pr_state(gh_command: str, main: Path, branch: str) -> str:
43
+ remote = run(
44
+ ["git", "remote", "get-url", "origin"],
45
+ cwd=main,
46
+ check=False,
47
+ )
48
+ if remote.returncode != 0:
49
+ return "none"
50
+ result = run(
51
+ [
52
+ gh_command, "pr", "list", "--state", "all", "--head", branch,
53
+ "--json", "number,state,mergedAt",
54
+ ],
55
+ cwd=main,
56
+ check=False,
57
+ )
58
+ if result.returncode != 0:
59
+ detail = (result.stderr or result.stdout).strip()
60
+ raise LifecycleError(f"cannot determine PR state for {branch}: {detail}")
61
+ try:
62
+ prs = json.loads(result.stdout)
63
+ except json.JSONDecodeError as error:
64
+ raise LifecycleError(f"cannot determine PR state for {branch}: invalid gh output") from error
65
+ if any(str(pr.get("state", "")).upper() == "OPEN" for pr in prs):
66
+ return "open"
67
+ if any(pr.get("mergedAt") or str(pr.get("state", "")).upper() == "MERGED" for pr in prs):
68
+ return "merged"
69
+ return "none"
70
+
71
+
72
+ def collect_assessment(profile, main: Path, worktree: Path, gh_command: str):
73
+ facts = collect_cleanup_facts(
74
+ main,
75
+ worktree,
76
+ )
77
+ state = pr_state(gh_command, main, facts.branch)
78
+ assessment = classify_cleanup(profile, replace(facts, pr_state=state))
79
+ try:
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
+ )
90
+
91
+
22
92
  def execute(args: argparse.Namespace) -> dict:
23
93
  main = main_worktree(Path.cwd())
24
94
  profile_path = Path(args.profile)
25
95
  if not profile_path.is_absolute():
26
96
  profile_path = main / profile_path
27
97
  profile = load_profile(profile_path)
28
- assessment = cleanup_assessment(profile, main, Path(args.worktree))
98
+ if not args.worktree:
99
+ raise LifecycleError("worktree is required")
100
+ worktree = Path(args.worktree)
101
+ assessment = collect_assessment(profile, main, worktree, args.gh_command)
29
102
  report = {
30
103
  "worktree": str(assessment.worktree),
31
104
  "branch": assessment.branch,
32
105
  "removable": assessment.removable,
33
106
  "reasons": list(assessment.reasons),
34
107
  "assumptions": assessment.assumptions,
108
+ "scratchFiles": list(assessment.scratch_files),
35
109
  "removed": False,
36
110
  }
37
111
  if not args.remove:
38
112
  return report
39
113
  if not assessment.removable:
40
114
  raise LifecycleError("; ".join(assessment.reasons))
41
- run(["git", "worktree", "remove", str(assessment.worktree)], cwd=main)
115
+ latest = collect_assessment(profile, main, worktree, args.gh_command)
116
+ if not latest.removable:
117
+ raise LifecycleError(f"cleanup changed before removal: {'; '.join(latest.reasons)}")
118
+ if (
119
+ latest.branch != assessment.branch
120
+ or latest.scratch_files != assessment.scratch_files
121
+ or latest.assumptions != assessment.assumptions
122
+ or latest.root_device != assessment.root_device
123
+ or latest.root_inode != assessment.root_inode
124
+ or latest.scratch_evidence != assessment.scratch_evidence
125
+ ):
126
+ raise LifecycleError("cleanup changed before removal: inventory no longer matches preview")
127
+ with verified_worktree_root(
128
+ latest.worktree,
129
+ latest.root_device,
130
+ latest.root_inode,
131
+ ) as root_descriptor:
132
+ remove_authorized_scratch(
133
+ profile,
134
+ root_descriptor,
135
+ latest.scratch_files,
136
+ assessment.scratch_evidence,
137
+ )
138
+ run(["git", "worktree", "remove", str(latest.worktree)], cwd=main)
42
139
  run(["git", "branch", "-d", assessment.branch], cwd=main)
43
140
  report["removed"] = True
44
141
  return report
45
142
 
46
143
 
144
+ def sweep(args: argparse.Namespace) -> dict:
145
+ main = main_worktree(Path.cwd())
146
+ profile_path = Path(args.profile)
147
+ if not profile_path.is_absolute():
148
+ profile_path = main / profile_path
149
+ profile = load_profile(profile_path)
150
+ report = collect_sweep(
151
+ profile,
152
+ main,
153
+ lambda branch: pr_state(args.gh_command, main, branch),
154
+ )
155
+ payload = asdict(report)
156
+ return {
157
+ "mainBranch": payload["main_branch"],
158
+ "worktreeCount": payload["worktree_count"],
159
+ "localBranchCount": payload["local_branch_count"],
160
+ "mergedRemoteBranchCount": payload["merged_remote_branch_count"],
161
+ "rows": [
162
+ {
163
+ "kind": row["kind"],
164
+ "path": row["path"],
165
+ "branch": row["branch"],
166
+ "issue": row["issue"],
167
+ "prState": row["pr_state"],
168
+ "mergedIntoMain": row["merged_into_main"],
169
+ "lastCommitAgeSeconds": row["last_commit_age_seconds"],
170
+ "removable": row["removable"],
171
+ "reasons": list(row["reasons"]),
172
+ "verdictReason": row["verdict_reason"],
173
+ "scratchFiles": list(row["scratch_files"]),
174
+ "assumptions": row["assumptions"],
175
+ }
176
+ for row in payload["rows"]
177
+ ],
178
+ }
179
+
180
+
47
181
  def main() -> int:
48
182
  try:
49
- print(json.dumps(execute(parse_args()), ensure_ascii=False, indent=2))
183
+ if len(sys.argv) > 1 and sys.argv[1] == "sweep":
184
+ result = sweep(parse_sweep_args(sys.argv[2:]))
185
+ else:
186
+ result = execute(parse_args())
187
+ print(json.dumps(result, ensure_ascii=False, indent=2))
50
188
  return 0
51
189
  except LifecycleError as error:
52
190
  print(f"STOP: {error}", file=sys.stderr)