groundfast 0.8.0 → 0.8.2

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 (55) hide show
  1. package/extensions/index.ts +1 -2
  2. package/package.json +3 -2
  3. package/references/coverage.md +26 -16
  4. package/references/pr-goal.md +19 -6
  5. package/scripts/cr-comment.sh +19 -6
  6. package/scripts/cr-status.sh +7 -7
  7. package/scripts/origin-guard.sh +25 -7
  8. package/scripts/redact.sh +40 -3
  9. package/scripts/require-timeout.sh +13 -0
  10. package/scripts/required-checks.sh +52 -0
  11. package/scripts/strip-shim.sh +2 -1
  12. package/skills/babysit/SKILL.md +3 -3
  13. package/skills/babysit/references/coderabbit.md +6 -3
  14. package/skills/babysit/references/loop.md +23 -2
  15. package/skills/babysit/scripts/cov-comment.sh +6 -3
  16. package/skills/babysit/scripts/cycle.sh +24 -12
  17. package/skills/babysit/scripts/flaky.sh +166 -0
  18. package/skills/babysit/scripts/gh-thread.sh +1 -1
  19. package/skills/babysit/scripts/pr-goal.sh +52 -14
  20. package/skills/babysit/scripts/pr-state.sh +6 -2
  21. package/skills/babysit/scripts/push-pr.sh +1 -0
  22. package/skills/babysit/scripts/stage.sh +6 -2
  23. package/skills/babysit/scripts/wait-ci.sh +22 -1
  24. package/skills/babysit/scripts/wait-review.sh +5 -1
  25. package/skills/drain/SKILL.md +3 -2
  26. package/skills/drain/references/inner-loop.md +3 -1
  27. package/skills/drain/scripts/conflict-finish.sh +5 -3
  28. package/skills/drain/scripts/drain-queue.sh +4 -3
  29. package/skills/drain/scripts/integrate-base.sh +31 -1
  30. package/skills/drain/scripts/merge-pr.sh +8 -2
  31. package/skills/drain/scripts/order-queue.sh +18 -10
  32. package/skills/drain/scripts/review-diff.sh +3 -1
  33. package/skills/pipeline/references/issue-contract.md +12 -0
  34. package/skills/pipeline/references/issue-loop.md +6 -4
  35. package/skills/pipeline/references/review-gate.md +1 -1
  36. package/skills/pipeline/scripts/claim-issue.sh +1 -1
  37. package/skills/pipeline/scripts/create-pr.sh +2 -1
  38. package/skills/pipeline/scripts/issue-context.sh +1 -1
  39. package/skills/pipeline/scripts/issue-note.sh +1 -1
  40. package/skills/pipeline/scripts/issue-queue.sh +70 -7
  41. package/skills/pipeline/scripts/pipeline-cmd.sh +14 -7
  42. package/skills/pipeline/scripts/prepare-issue.sh +1 -0
  43. package/skills/pr/scripts/pre-pr-state.sh +8 -2
  44. package/skills/pr/scripts/push-branch.sh +13 -0
  45. package/skills/scaffolding-services/scripts/detect-stack.sh +28 -2
  46. package/skills/scoping-engagement/SKILL.md +1 -1
  47. package/skills/ship/references/plugin.md +2 -2
  48. package/skills/ship/scripts/repo-state.sh +5 -1
  49. package/skills/tidy/SKILL.md +2 -2
  50. package/skills/tidy/scripts/drop-stash.sh +26 -0
  51. package/skills/tidy/scripts/orphans.sh +34 -13
  52. package/skills/wrap/scripts/repo-state.sh +6 -2
  53. package/skills/wrap/scripts/session-cover.sh +8 -9
  54. package/skills/wrap/scripts/tasks.sh +10 -4
  55. package/skills/wrap/scripts/verify.sh +17 -2
@@ -12,6 +12,8 @@ set -uo pipefail
12
12
 
13
13
  # shellcheck source=../../../scripts/redact.sh
14
14
  . "$(dirname "$0")/../../../scripts/redact.sh"
15
+ # shellcheck source=../../../scripts/strip-shim.sh
16
+ . "$(dirname "$0")/../../../scripts/strip-shim.sh"
15
17
 
16
18
  git rev-parse --is-inside-work-tree >/dev/null 2>&1 || { echo "(fora de um repositório git)"; exit 0; }
17
19
 
@@ -36,19 +38,25 @@ git worktree list --porcelain | sed -n 's/^worktree //p' | while IFS= read -r wt
36
38
  ahead=$(git -C "$wt" rev-list --count '@{upstream}..HEAD' 2>/dev/null || echo '?')
37
39
  if [ -z "$clean" ] && [ -n "$track" ] && [ "$ahead" = "0" ]; then
38
40
  cls=AUTO; why="tree limpa e ${br:-HEAD} já pushada em $track"
41
+ rm='git worktree remove'
39
42
  else
40
43
  cls=GATE
41
44
  why="${clean:+conteúdo local não salvo (inclui ignorados)}${clean:+; }${track:-sem upstream}${track:+, $ahead commit(s) não pushado(s)}"
45
+ # GATE aprovado descarta conteúdo local: sem --force o git recusa (rc 128).
46
+ rm='git worktree remove --force'
42
47
  fi
43
48
  printf '%s %s @%s — %s\n' "$cls" "$wt" "$sha" "$why" | redact
44
- printf ' remover: git worktree remove %q\n' "$wt" | redact
49
+ printf ' remover: %s %q\n' "$rm" "$wt" | redact
45
50
  printf ' recuperar: git worktree add %q %q\n' "$wt" "${br:-$sha}" | redact
46
51
  done
47
52
  echo
48
53
 
49
54
  echo "## Branches locais"
50
- git for-each-ref --format='%(refname:short)|%(objectname:short)|%(upstream:short)|%(upstream:track)' refs/heads |
51
- while IFS='|' read -r br sha up track; do
55
+ gh_err=$(mktemp); trap 'rm -f "$gh_err"' EXIT
56
+ # Tab separa os campos: `|` é legal em nome de branch (`a|b` viraria a branch
57
+ # `a`, e o `remover:` apontaria para outra), tab não é. Campo vazio só no fim.
58
+ git for-each-ref --format='%(refname:short)%09%(objectname:short)%09%(upstream:short)%09%(upstream:track)' refs/heads |
59
+ while IFS=$'\t' read -r br sha up track; do
52
60
  [ "$br" = "$head_branch" ] && continue
53
61
  git worktree list --porcelain | grep -qxF -- "branch refs/heads/$br" && continue
54
62
 
@@ -56,20 +64,31 @@ while IFS='|' read -r br sha up track; do
56
64
  if [ -n "$up" ]; then
57
65
  why="upstream $up${track:+ $track}"
58
66
  if [ "$gh_ok" -eq 1 ]; then
59
- st=$(gh pr view "$br" --json state --jq .state 2>/dev/null)
67
+ pr=$(gh pr view "$br" --json state,headRefOid,number --jq '"\(.state) \(.headRefOid) \(.number)"' 2>"$gh_err" | strip_shim)
68
+ read -r st merged prnum <<<"$pr"
60
69
  case "$st" in
61
70
  MERGED)
62
- # Merged só basta se não houver commit local à frente do upstream:
63
- # `branch -D` sobre uma branch ahead deixa esses commits só no reflog.
64
- case "$track" in
65
- *ahead*) why="PR MERGED, mas $track — há commit local não pushado" ;;
66
- *) cls=AUTO; why="PR MERGED no remote e nada à frente de $up" ;;
67
- esac
71
+ # Merged só basta se todo commit local está no head que o PR mergeou:
72
+ # `branch -D` deixa o resto só no reflog. O track não serve de prova —
73
+ # com --delete-branch o upstream fica [gone] e o ahead some da conta.
74
+ if ! [[ $merged =~ ^[0-9a-f]{40}$ ]] || ! git cat-file -e "$merged^{commit}" 2>/dev/null; then
75
+ why="PR MERGED, mas o head mergeado não está neste clone — nada confirma o local"
76
+ [[ $prnum =~ ^[1-9][0-9]*$ ]] && why="$why; confirme com: git fetch origin refs/pull/$prnum/head"
77
+ elif git merge-base --is-ancestor "refs/heads/$br" "$merged"; then
78
+ cls=AUTO; why="PR MERGED e todo commit local está no head mergeado (${merged:0:7})"
79
+ else
80
+ why="PR MERGED, mas há commit local fora do head mergeado${track:+ ($track)}"
81
+ fi
68
82
  ;;
69
83
  # CLOSED é PR fechado SEM merge: trabalho rejeitado ou abandonado, cujo
70
84
  # único registro pode ser esta branch. O remote não confirma nada.
71
85
  CLOSED) why="PR CLOSED sem merge — o trabalho não foi para lugar nenhum" ;;
72
- "") why="$why; sem PR encontrado" ;;
86
+ # Falha de rede ou de API não é "sem PR": o motivo diz qual das duas foi.
87
+ # O shim do mise pode pôr o banner no stderr também; -a e LC_ALL=C: byte
88
+ # fora de UTF-8 faria o grep ver binário e apagar o motivo.
89
+ "") errs=$(strip_shim <"$gh_err")
90
+ if printf '%s\n' "$errs" | grep -q '^no pull requests found'; then why="$why; sem PR encontrado"
91
+ else why="$why; gh falhou ao consultar o PR: $(printf '%s\n' "${errs:-sem stderr}" | LC_ALL=C grep -a -m1 . | REDACT_MAXCOL=120 redact)"; fi ;;
73
92
  *) why="$why; PR $st — ainda aberto" ;;
74
93
  esac
75
94
  fi
@@ -82,9 +101,11 @@ echo
82
101
 
83
102
  echo "## Stash"
84
103
  # Stash nunca é AUTO: depois do drop, a recuperação depende do reflog, que expira.
85
- git stash list --format='%gd|%H|%cr|%gs' | while IFS='|' read -r ref sha age subj; do
104
+ # O remover: é por sha (drop-stash.sh relê a pilha), então a ordem não importa.
105
+ git stash list --format='%gd|%H|%cr|%gs' |
106
+ while IFS='|' read -r ref sha age subj; do
86
107
  printf 'GATE %s @%s — %s — %s\n' "$ref" "${sha:0:7}" "$age" "$subj" | redact
87
- printf ' remover: git stash drop %s\n' "$ref" | redact
108
+ printf ' remover: scripts/drop-stash.sh %s\n' "$sha" | redact
88
109
  printf ' recuperar: git stash apply %s\n' "$sha" | redact
89
110
  done
90
111
  exit 0
@@ -56,7 +56,11 @@ done
56
56
 
57
57
  echo
58
58
  echo "## Working tree"
59
- git status --porcelain 2>/dev/null | head -40 | body "(limpo)"
59
+ all=$(git status --porcelain 2>/dev/null)
60
+ printf '%s' "$all" | head -40 | body "(limpo)"
61
+ # O corte é dito: o passo 2 separa as mudanças desta sessão das alheias por aqui.
62
+ extra=$(( $(printf '%s\n' "$all" | wc -l) - 40 ))
63
+ [ "$extra" -gt 0 ] && echo "(+$extra linha(s) fora do corte — git status --porcelain mostra todas)"
60
64
 
61
65
  echo
62
66
  echo "## Diff vs HEAD (linhas por arquivo)"
@@ -106,5 +110,5 @@ fi
106
110
 
107
111
  echo
108
112
  echo "## Plan files da sessão (7 dias)"
109
- find "$HOME/.claude/plans" -maxdepth 1 -name '*.md' -mtime -7 2>/dev/null | head -10 | body "(nenhum)"
113
+ find "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/plans" -maxdepth 1 -name '*.md' -mtime -7 2>/dev/null | head -10 | body "(nenhum)"
110
114
  exit 0
@@ -32,15 +32,14 @@ if [ -z "$abs" ]; then
32
32
  *)
33
33
  host=${host:-claude}
34
34
  base=${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd -P)}
35
- for slug in \
36
- "$(printf '%s' "$base" | sed 's#^/##; s#/#-#g')" \
37
- "$(printf '%s' "$base" | sed 's#^/##; s#[/._]#-#g')"
38
- do
39
- cand="$HOME/.claude/projects/-$slug/$sid.jsonl"
40
- [ -f "$cand" ] || continue
41
- abs=$cand
42
- break
43
- done
35
+ # O Claude Code grava em ${CLAUDE_CONFIG_DIR:-~/.claude}/projects/<slug>,
36
+ # com cada unidade UTF-16 fora de [a-zA-Z0-9] trocada por `-`. Acima de
37
+ # 200 caracteres ele soma um hash ao slug, e a busca não acha: sai
38
+ # not_found. O sed reproduz a regra para texto BMP em locale UTF-8; fora
39
+ # do BMP (emoji) ou em C/POSIX a contagem de `-` diverge e sai not_found.
40
+ slug=$(printf '%s' "$base" | sed 's/[^0-9ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz]/-/g')
41
+ cand="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/projects/$slug/$sid.jsonl"
42
+ [ -f "$cand" ] && abs=$cand
44
43
  if [ -z "$abs" ]; then
45
44
  echo "coverage=unavailable $empty reasons=session_file_not_found"
46
45
  exit 0
@@ -58,12 +58,18 @@ case "$cmd" in
58
58
  [ -s "$tmp" ] || die "stdin vazio — nada para apender"
59
59
 
60
60
  # Valida TODAS as linhas antes de escrever uma: um append parcial deixaria
61
- # o ledger num estado que a próxima sessão não sabe interpretar.
62
- line=0
63
- while IFS= read -r l; do
61
+ # o ledger num estado que a próxima sessão não sabe interpretar. O `|| [ -n ]`
62
+ # entrega a última linha sem `\n`, que o `jq -c` abaixo apenda do mesmo jeito.
63
+ line=0; added=0
64
+ while IFS= read -r l || [ -n "$l" ]; do
64
65
  line=$((line + 1))
65
66
  [ -n "${l//[[:space:]]/}" ] || continue
67
+ added=$((added + 1)) # linha em branco não vira evento no `jq -c` abaixo
66
68
  printf '%s' "$l" | jq -e . >/dev/null 2>&1 || die "linha $line não é JSON válido"
69
+ # Um objeto por linha: com dois valores, o `jq -c` abaixo gravaria os dois
70
+ # e só o primeiro passaria pelos campos.
71
+ printf '%s' "$l" | jq -se 'length == 1 and (.[0] | type == "object")' >/dev/null 2>&1 \
72
+ || die "linha $line: mais de um valor JSON, ou valor que não é objeto"
67
73
  id=$(printf '%s' "$l" | jq -r '.id // empty')
68
74
  st=$(printf '%s' "$l" | jq -r '.state // empty')
69
75
  ti=$(printf '%s' "$l" | jq -r '.title // empty')
@@ -76,7 +82,7 @@ case "$cmd" in
76
82
 
77
83
  # Compacta para uma linha por evento — o formato do ledger é JSONL.
78
84
  jq -c . <"$tmp" >>"$file" || die "append falhou"
79
- echo "ok: $line linha(s) apendada(s) em $file"
85
+ echo "ok: $added linha(s) apendada(s) em $file"
80
86
  ;;
81
87
 
82
88
  *) die "subcomando desconhecido: $cmd" ;;
@@ -6,7 +6,7 @@
6
6
  set -uo pipefail
7
7
 
8
8
  root=${1:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}
9
- mem_root="$HOME/.claude/projects"
9
+ mem_root="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/projects"
10
10
  here=$(cd "$(dirname "$0")" && pwd -P)
11
11
  fails=0
12
12
 
@@ -160,9 +160,24 @@ fi
160
160
  # slug não é derivável daqui: procuramos o caminho do projeto em todos eles.
161
161
  # No CI o runner é efêmero e não tem memória nativa, e o check mede o ritual
162
162
  # local. Variável própria, não `CI`: uma sessão local com `CI` exportado não pula.
163
+ # Caminho inteiro, não prefixo: `/x/repo-site` não é ponteiro de `/x/repo`. O
164
+ # caractere depois do caminho não pode continuar o nome (`/` é subcaminho e vale);
165
+ # `.` só continua quando vem seguido de nome (`/x/repo.bak`), não no fim da frase.
166
+ mentions_root() {
167
+ local f
168
+ while IFS= read -r f; do
169
+ ROOT=$root awk '{ s = $0; r = ENVIRON["ROOT"]
170
+ while ((i = index(s, r)) > 0) {
171
+ c = substr(s, i + length(r), 1); n = substr(s, i + length(r) + 1, 1)
172
+ if (c !~ /[A-Za-z0-9_-]/ && !(c == "." && n ~ /[A-Za-z0-9_-]/)) { found = 1; exit }
173
+ s = substr(s, i + 1)
174
+ } } END { exit !found }' "$f" && return 0
175
+ done < <(grep -rlF -- "$root" "$mem_root"/*/memory 2>/dev/null)
176
+ return 1
177
+ }
163
178
  if [ "${GROUNDFAST_CI-}" = 1 ]; then
164
179
  skip "memória nativa não se aplica no CI (GROUNDFAST_CI=1)"
165
- elif [ -d "$mem_root" ] && grep -rqlF -- "$root" "$mem_root"/*/memory 2>/dev/null; then
180
+ elif [ -d "$mem_root" ] && mentions_root; then
166
181
  pass "memória nativa tem ponteiro para $root"
167
182
  else
168
183
  fail "nenhum arquivo em $mem_root/*/memory menciona $root"