@ikon85/agent-workflow-kit 0.41.0 → 0.41.1

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/README.md CHANGED
@@ -469,6 +469,16 @@ the old way. Decision record:
469
469
 
470
470
  ## Release notes
471
471
 
472
+ ### 0.41.1
473
+
474
+ - changed: `README.md`
475
+ - changed: `agent-workflow-kit.package.json`
476
+ - changed: `package.json`
477
+ - changed: `scripts/pr-body-check.py`
478
+ - changed: `scripts/test_pr_body_check.py`
479
+ - changed: `scripts/test_worktree_wrapup_contract.py`
480
+ - changed: `scripts/wrapup-land.py`
481
+
472
482
  ### 0.41.0
473
483
 
474
484
  - added: `docs/adr/0009-teardown-authority-is-stateless-repository-classification.md`
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitVersion": "0.41.0",
2
+ "kitVersion": "0.41.1",
3
3
  "files": [
4
4
  {
5
5
  "path": ".agents/skills/ask-matt/SKILL.md",
@@ -2627,7 +2627,7 @@
2627
2627
  "path": "scripts/pr-body-check.py",
2628
2628
  "kind": "script",
2629
2629
  "installRole": "consumer",
2630
- "sha256": "7fa67a672d6f6986b0fba99e78157d51225ab91f61b691a4cc184d316b521882",
2630
+ "sha256": "d18a9f1bf9285506bd7d40a1800dd9b66f631bd4666f0ae801e8db1882d3ad2d",
2631
2631
  "mode": 493,
2632
2632
  "origin": "kit"
2633
2633
  },
@@ -2867,7 +2867,7 @@
2867
2867
  "path": "scripts/wrapup-land.py",
2868
2868
  "kind": "script",
2869
2869
  "installRole": "consumer",
2870
- "sha256": "476012558bb786070ce47458c6f0734ff110d5d957af72e13b20d49a37a03592",
2870
+ "sha256": "de41f6c7bee1f67f3f25db4c7750a3caca422be5749012c6d5402cfeed91b2f7",
2871
2871
  "mode": 493,
2872
2872
  "origin": "kit"
2873
2873
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ikon85/agent-workflow-kit",
3
- "version": "0.41.0",
3
+ "version": "0.41.1",
4
4
  "description": "Portable AI-agent workflow skills (plan → execute → land → learn) for Claude Code & Codex — grilling, TDD, diagnosis, two-axis code review, cross-model Codex review, design & domain-modeling, plus a skill router (ask-matt). npx init/update/diff/uninstall.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -125,6 +125,21 @@ def _close_on(body: str, n: int) -> bool:
125
125
  return bool(pat.search(strip_code(body)))
126
126
 
127
127
 
128
+ def active_close_targets(body: str) -> list:
129
+ """All issue numbers an ACTIVE close-keyword targets (outside code spans),
130
+ as sorted, deduplicated strings. Same grammar `_close_on` enforces —
131
+ plain `closes #n`, the colon form, and the full-URL form. This is the one
132
+ close grammar; close-verification (wrapup-land Step 5b) must derive its
133
+ targets from here, never from a branch number."""
134
+ text = strip_code(body)
135
+ pat = re.compile(
136
+ rf"\b(?:{CLOSE_KEYWORDS})(?::\s*|\s+)"
137
+ rf"(?:#0*(\d+)(?!\d)|https?://\S+/issues/0*(\d+)(?!\d))",
138
+ re.IGNORECASE)
139
+ found = {a or b for a, b in pat.findall(text)}
140
+ return sorted(found, key=int)
141
+
142
+
128
143
  def _part_of(body: str, n: int) -> bool:
129
144
  return bool(re.search(
130
145
  rf"\b{PART_OF_RE_SRC}(?::\s*|\s+)#0*{n}(?!\d)", body or "", re.IGNORECASE))
@@ -79,6 +79,15 @@ class ExistingBodyRules(unittest.TestCase):
79
79
  def test_retro_line_still_required(self):
80
80
  self.assertTrue(any("Retro" in item for item in pbc.check_pr_body("closes #149", 149, None)))
81
81
 
82
+ def test_active_close_targets_extracts_all_forms(self):
83
+ body = ("closes #12\nFixes: #13\n"
84
+ "resolves https://github.com/iKon85/agent-workflow-kit/issues/14\n"
85
+ "`closes #99` and Part of #320 and closes #12")
86
+ self.assertEqual(pbc.active_close_targets(body), ["12", "13", "14"])
87
+
88
+ def test_active_close_targets_empty_for_part_of_only(self):
89
+ self.assertEqual(pbc.active_close_targets("Part of #320\n**Retro:** skipped — x"), [])
90
+
82
91
  def test_program_prd_label_counts_as_anchor(self):
83
92
  with mock.patch.object(pbc, "_run",
84
93
  return_value=(0, "type:program\npriority:medium")):
@@ -251,6 +251,13 @@ def downgrade_landing_attempt_to_v1(core, worktree: Path) -> Path:
251
251
 
252
252
 
253
253
  class WorktreeCleanupContract(unittest.TestCase):
254
+ def test_declared_close_targets_is_pr_body_authority(self):
255
+ mod = load_wrapup()
256
+ self.assertEqual(mod.declared_close_targets("Part of #320"), [])
257
+ self.assertEqual(mod.declared_close_targets("closes #341\ncloses #12"),
258
+ ["12", "341"])
259
+ self.assertEqual(mod.declared_close_targets("`closes #341`"), [])
260
+
254
261
  def test_profile_scratch_and_generated_evidence_share_safe_removal_contract(self):
255
262
  wrapup = load_wrapup()
256
263
  with tempfile.TemporaryDirectory() as tmp:
@@ -423,6 +423,18 @@ def issue_from_branch(branch: str) -> str | None:
423
423
  return m.group(2) if m else None
424
424
 
425
425
 
426
+ def declared_close_targets(body: str) -> list:
427
+ """Close authority for Step 5b: the PR body's ACTIVE close keywords, parsed
428
+ by pr-body-check's one close grammar. A branch number is context, never
429
+ closure authority — branch-derived closing once shut a Program-PRD that a
430
+ PR deliberately referenced only via `Part of`."""
431
+ spec = importlib.util.spec_from_file_location(
432
+ "pr_body_check_grammar", Path(__file__).parent / "pr-body-check.py")
433
+ mod = importlib.util.module_from_spec(spec)
434
+ spec.loader.exec_module(mod)
435
+ return mod.active_close_targets(body)
436
+
437
+
426
438
  def load_profile() -> dict:
427
439
  try:
428
440
  from board_config import load_board_config
@@ -1245,16 +1257,26 @@ def cmd_land(args) -> dict:
1245
1257
  elif p.returncode != 0:
1246
1258
  report["warnings"].append(f"branch -d {branch} refused: {(p.stderr or '').strip()[:200]}")
1247
1259
 
1248
- # Step 5b — verify issue auto-close (backtick-swallowed `closes` misses)
1260
+ # Step 5b — verify declared auto-closes (backtick-swallowed `closes`
1261
+ # misses). Targets come from the merged PR body's close keywords, never
1262
+ # from the branch number.
1249
1263
  issue = issue_from_branch(branch)
1250
- if issue:
1251
- p = run(["gh", "issue", "view", issue, "--json", "state"])
1252
- if p.returncode == 0 and json.loads(p.stdout)["state"] == "OPEN":
1253
- run(["gh", "issue", "close", issue, "-c",
1254
- f"Merged via PR #{pr} auto-close didn't fire; closed manually."])
1255
- report["issue_close"] = f"#{issue} closed manually"
1256
- elif p.returncode == 0:
1257
- report["issue_close"] = f"#{issue} already closed"
1264
+ p = run(["gh", "pr", "view", pr, "--json", "body"])
1265
+ merged_body = (json.loads(p.stdout).get("body") or "") if p.returncode == 0 else ""
1266
+ targets = declared_close_targets(merged_body)
1267
+ if not targets:
1268
+ report["issue_close"] = "no close targets declared nothing to verify"
1269
+ else:
1270
+ states = []
1271
+ for t in targets:
1272
+ q = run(["gh", "issue", "view", t, "--json", "state"])
1273
+ if q.returncode == 0 and json.loads(q.stdout)["state"] == "OPEN":
1274
+ run(["gh", "issue", "close", t, "-c",
1275
+ f"Merged via PR #{pr} — auto-close didn't fire; closed manually."])
1276
+ states.append(f"#{t} closed manually")
1277
+ elif q.returncode == 0:
1278
+ states.append(f"#{t} already closed")
1279
+ report["issue_close"] = " · ".join(states)
1258
1280
 
1259
1281
  # Step 5c — local merged-branch sweep (-d only: unreachable-from-main is refused)
1260
1282
  swept = []