@spardutti/claude-skills 2.60.3 → 2.60.5

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.
@@ -80,11 +80,18 @@ git rev-parse --git-dir >/dev/null 2>&1 || exit 0
80
80
  pr_branch() {
81
81
  case "$CMD" in
82
82
  *"gh pr create"*) printf '%s\n' "$CMD" | sed -nE 's/.*(--head[ =]|-H )"?([^ ;&|"]+).*/\2/p' ;;
83
- *"gh pr merge"*) merge_branch ;;
83
+ *"gh pr merge"*) merge_field headRefName ;;
84
84
  esac
85
85
  }
86
86
 
87
- merge_branch() {
87
+ pr_base() {
88
+ case "$CMD" in
89
+ *"gh pr create"*) printf '%s\n' "$CMD" | sed -nE 's/.*(--base[ =]|-B )"?([^ ;&|"]+).*/\2/p' ;;
90
+ *"gh pr merge"*) merge_field baseRefName ;;
91
+ esac
92
+ }
93
+
94
+ merge_field() { # merge_field <gh pr field>
88
95
  local pr="" repo="" next=""
89
96
  for t in $(printf '%s\n' "$CMD" | sed 's/.*gh pr merge//; s/[;&|].*//'); do
90
97
  if [ "$next" = repo ]; then repo=$t; next=""; continue; fi
@@ -96,7 +103,7 @@ merge_branch() {
96
103
  esac
97
104
  done
98
105
  [ -n "$pr" ] || return 0
99
- gh pr view "$pr" ${repo:+-R "$repo"} --json headRefName -q .headRefName 2>/dev/null
106
+ gh pr view "$pr" ${repo:+-R "$repo"} --json "$1" -q ".$1" 2>/dev/null
100
107
  }
101
108
 
102
109
  worktree_of() {
@@ -111,6 +118,11 @@ case "$CMD" in *"gh pr create"*)
111
118
  CWD_TOP=$(git -C "${CWD:-.}" rev-parse --show-toplevel 2>/dev/null) && [ -n "$CWD_TOP" ] && TARGET="$CWD_TOP" ;;
112
119
  esac
113
120
  BRANCH=$(pr_branch)
121
+ # A release PR carries only work each feature PR already gated, and the gate on the
122
+ # trunk measures it against itself, so a receipt there proves nothing.
123
+ case "$BRANCH" in dev|develop|development)
124
+ case "$(pr_base)" in main|master) exit 0 ;; esac ;;
125
+ esac
114
126
  [ -n "$BRANCH" ] && WT=$(worktree_of "$BRANCH") && [ -n "$WT" ] && TARGET="$WT"
115
127
 
116
128
  # Ask the gate for the key rather than recomputing it here. Two implementations
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "node:child_process";
3
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { basename, dirname, join, resolve } from "node:path";
5
5
  import { git, parseReport, prSize } from "./ship-log-parse.mjs";
6
6
  import { printReport, tally } from "./ship-log-report.mjs";
7
7
 
8
8
  const HERE = dirname(resolve(process.argv[1]));
9
9
  const VERDICTS = ["confirmed", "refuted", "unconfirmed", "skipped"];
10
+ // A release ships only work its feature PRs already logged.
11
+ const RELEASE = /^(main|master|dev|develop|development|chore\/release-.*)$/;
10
12
  const now = () => new Date().toISOString().replace(/\.\d+Z$/, "Z");
11
13
 
12
14
  // gate-config.sh owns the config files and their order, so the gate and this agree on "on".
@@ -45,7 +47,19 @@ function summarize(record) {
45
47
  return { minutes: Math.round(minutes * 10) / 10, gateRuns: record.gate.length, gateSeconds: record.gate.reduce((n, r) => n + (r.seconds ?? 0), 0), findings: by };
46
48
  }
47
49
 
50
+ // Survivors fixed but never logged: a FAIL that a later PASS cleared, with no mutation finding since.
51
+ function settleSurvivors(record) {
52
+ const i = record.gate.findLastIndex((g) => g.verdict === "FAIL" && g.projects?.some((p) => p.survivors?.length));
53
+ if (i < 0 || !record.gate.slice(i + 1).some((g) => g.verdict === "PASS")) return;
54
+ if (record.findings.some((f) => f.by === "mutation" && f.ts >= record.gate[i].ts)) return;
55
+ for (const s of record.gate[i].projects.flatMap((p) => p.survivors ?? [])) {
56
+ record.findings.push({ ts: now(), by: "mutation", what: `${s.mutator ?? s.kind} at ${s.at}`, file: s.at.split(":")[0],
57
+ verdict: "confirmed", fixed: true, note: "logged by ship-log: a later gate run passed" });
58
+ }
59
+ }
60
+
48
61
  function finalize(dir, path, record, tag, status, reason, at) {
62
+ settleSurvivors(record);
49
63
  Object.assign(record, { status, reason: reason ?? null, finished: at ?? now() });
50
64
  record.summary = summarize(record);
51
65
  const stem = `${record.app}_${record.finished.slice(0, 10)}_${tag}`;
@@ -57,7 +71,7 @@ function finalize(dir, path, record, tag, status, reason, at) {
57
71
  }
58
72
 
59
73
  function prOf(branch) {
60
- const r = spawnSync("gh", ["pr", "list", "--head", branch, "--state", "all", "--json", "number,createdAt,state", "--limit", "1"], { encoding: "utf8" });
74
+ const r = spawnSync("gh", ["pr", "list", "--head", branch, "--state", "all", "--json", "number,createdAt,closedAt,state", "--limit", "1"], { encoding: "utf8" });
61
75
  try { return r.status === 0 ? JSON.parse(r.stdout)[0] : undefined; } catch { return undefined; }
62
76
  }
63
77
 
@@ -68,8 +82,10 @@ function closeOpened(dir, app) {
68
82
  const open = join(dir, "open");
69
83
  for (const f of existsSync(open) ? readdirSync(open).filter((n) => n.startsWith(`${app}__`)) : []) {
70
84
  const path = join(open, f), record = JSON.parse(readFileSync(path, "utf8"));
71
- if (!record.gate.length && !record.findings.length) continue;
72
- const pr = prOf(record.branch);
85
+ const pr = prOf(record.branch), empty = !record.gate.length && !record.findings.length;
86
+ // A /ship that only merged fills nothing, so once its PR closes there is nothing to keep.
87
+ if (empty && pr?.closedAt >= record.started) { rmSync(path); continue; }
88
+ if (empty) continue;
73
89
  const opened = pr && pr.createdAt >= record.started;
74
90
  if (!pr || (!opened && pr.state !== "OPEN")) continue;
75
91
  finalize(dir, path, Object.assign(record, { pr: pr.number }), `pr${pr.number}`,
@@ -144,6 +160,11 @@ function run(mode, args) {
144
160
  if (mode === "on") return c.on ? 0 : 1;
145
161
  if (!c.on) return 0;
146
162
  const id = identity(c.root), path = openPath(c.dir, id), record = load(path, id);
163
+ if (mode === "start" && RELEASE.test(id.branch)) {
164
+ closeOpened(c.dir, id.app);
165
+ if (existsSync(path)) rmSync(path);
166
+ return 0;
167
+ }
147
168
  if (mode === "start") return restart(c.dir, path, record, id), 0;
148
169
  if (mode === "ready") {
149
170
  const todo = existsSync(path) ? missing(record, c) : [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "2.60.3",
3
+ "version": "2.60.5",
4
4
  "description": "Guardrails for Claude Code — best-practice skills, planning commands, and delivery gates.",
5
5
  "type": "module",
6
6
  "repository": {