chore: import upstream snapshot with attribution
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PostTaskInput is the JSON structure Claude Code sends to Stop hooks.
|
||||
// stop_hook_active is true when the hook is already being rerun (another
|
||||
// Stop hook asked the agent to continue) — we must skip in that case to
|
||||
// avoid recursion.
|
||||
type PostTaskInput struct {
|
||||
HookEventName string `json:"hook_event_name"`
|
||||
SessionID string `json:"session_id"`
|
||||
TranscriptPath string `json:"transcript_path"`
|
||||
CWD string `json:"cwd"`
|
||||
StopHookActive bool `json:"stop_hook_active"`
|
||||
}
|
||||
|
||||
// runPostTask handles a Stop hook invocation with the raw stdin bytes.
|
||||
// It runs diagnostics on any changed symbols (dead-code, test targets,
|
||||
// guards, contract violations) and injects the findings as
|
||||
// additionalContext so the agent self-corrects before handing off.
|
||||
//
|
||||
// Degrades silently when: the bridge is unreachable, there are no
|
||||
// changes, or stop_hook_active is true.
|
||||
func runPostTask(data []byte, port int) {
|
||||
var input PostTaskInput
|
||||
if err := json.Unmarshal(data, &input); err != nil {
|
||||
return
|
||||
}
|
||||
if input.HookEventName != "Stop" {
|
||||
return
|
||||
}
|
||||
// Prevent recursion — if we're already rerunning a Stop hook, don't fire again.
|
||||
if input.StopHookActive {
|
||||
return
|
||||
}
|
||||
|
||||
briefing := buildPostTaskBriefing(port)
|
||||
if briefing == "" {
|
||||
return
|
||||
}
|
||||
|
||||
output := HookOutput{
|
||||
HookSpecificOutput: &HookSpecificOutput{
|
||||
HookEventName: "Stop",
|
||||
AdditionalContext: briefing,
|
||||
},
|
||||
}
|
||||
out, err := json.Marshal(output)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Print(string(out))
|
||||
}
|
||||
|
||||
// buildPostTaskBriefing runs diagnostics on the current working tree and
|
||||
// returns a compact markdown summary. Returns empty string when there's
|
||||
// nothing to report or the bridge is unreachable.
|
||||
func buildPostTaskBriefing(port int) string {
|
||||
raw := callServerTool(port, "detect_changes", map[string]any{
|
||||
"scope": "unstaged",
|
||||
})
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
var changes struct {
|
||||
ChangedFiles []string `json:"changed_files"`
|
||||
ChangedSymbols []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind"`
|
||||
} `json:"changed_symbols"`
|
||||
Risk string `json:"risk"`
|
||||
Summary string `json:"summary"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(raw), &changes); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(changes.ChangedSymbols) == 0 {
|
||||
// No indexed symbols touched — skip silently. The agent doesn't need
|
||||
// a post-task briefing when nothing meaningful changed in the graph.
|
||||
return ""
|
||||
}
|
||||
|
||||
ids := make([]string, len(changes.ChangedSymbols))
|
||||
for i, cs := range changes.ChangedSymbols {
|
||||
ids[i] = cs.ID
|
||||
}
|
||||
idsCSV := strings.Join(ids, ",")
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("## Gortex Post-Task Diagnostics\n\n")
|
||||
fmt.Fprintf(&sb, "**Changed:** %d symbols across %d files — risk `%s`.\n\n",
|
||||
len(changes.ChangedSymbols), len(changes.ChangedFiles), changes.Risk)
|
||||
|
||||
// Test targets — what to run.
|
||||
if tests := renderTestTargets(port, idsCSV); tests != "" {
|
||||
sb.WriteString("### Tests to Run\n\n")
|
||||
sb.WriteString(tests)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// Guard rule violations.
|
||||
if guards := renderGuardViolations(port, idsCSV); guards != "" {
|
||||
sb.WriteString("### Guard Violations\n\n")
|
||||
sb.WriteString(guards)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// Dead code — specifically whether any of the changed symbols are now orphaned.
|
||||
if dead := renderDeadCodeHits(port, ids); dead != "" {
|
||||
sb.WriteString("### Potential Dead Code (among changed symbols)\n\n")
|
||||
sb.WriteString(dead)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// Coverage gaps among changed symbols. Silent when the graph carries
|
||||
// no coverage_pct meta (the analyzer returns "no coverage gaps
|
||||
// matched" or just an empty body) — we don't want to nag when the
|
||||
// repo simply doesn't run `gortex enrich coverage`.
|
||||
if gaps := renderCoverageGapsHits(port, ids); gaps != "" {
|
||||
sb.WriteString("### Coverage Gaps (among changed symbols)\n\n")
|
||||
sb.WriteString(gaps)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// Stale-flag regression check. Silent unless one of the changed
|
||||
// symbols is itself a flag site or toggles one — pointing the agent
|
||||
// at flag rollouts they may have just touched.
|
||||
if flags := renderStaleFlagHits(port, ids); flags != "" {
|
||||
sb.WriteString("### Stale Flag Sites Touched\n\n")
|
||||
sb.WriteString(flags)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// Contract mismatches.
|
||||
if contracts := renderContractMismatches(port); contracts != "" {
|
||||
sb.WriteString("### API Contract Issues\n\n")
|
||||
sb.WriteString(contracts)
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
sb.WriteString("_Run the tests above and review any flagged items before handoff._\n")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// renderTestTargets asks the bridge for test files that exercise the changed symbols.
|
||||
func renderTestTargets(port int, idsCSV string) string {
|
||||
raw := callServerTool(port, "get_test_targets", map[string]any{
|
||||
"ids": idsCSV,
|
||||
"compact": true,
|
||||
})
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" || raw == "no test targets found" {
|
||||
return ""
|
||||
}
|
||||
return cappedLines(raw, 15)
|
||||
}
|
||||
|
||||
// renderGuardViolations asks the bridge for .gortex.yaml guard rule violations.
|
||||
func renderGuardViolations(port int, idsCSV string) string {
|
||||
raw := callServerTool(port, "check_guards", map[string]any{
|
||||
"ids": idsCSV,
|
||||
"compact": true,
|
||||
})
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" || strings.HasPrefix(raw, "no guard rule violations") {
|
||||
return ""
|
||||
}
|
||||
return cappedLines(raw, 10)
|
||||
}
|
||||
|
||||
// renderDeadCodeHits filters analyze:dead_code results to the intersection
|
||||
// with the currently-changed symbols (i.e. "did this task leave anything
|
||||
// orphaned"). Emits nothing when the intersection is empty.
|
||||
func renderDeadCodeHits(port int, ids []string) string {
|
||||
raw := callServerTool(port, "analyze", map[string]any{
|
||||
"kind": "dead_code",
|
||||
"compact": true,
|
||||
})
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
idSet := make(map[string]bool, len(ids))
|
||||
for _, id := range ids {
|
||||
idSet[id] = true
|
||||
}
|
||||
|
||||
var hits []string
|
||||
for _, line := range strings.Split(raw, "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
// Each compact line starts with the symbol descriptor including the
|
||||
// file path — we substring-match any of the changed IDs.
|
||||
for id := range idSet {
|
||||
if strings.Contains(line, id) {
|
||||
hits = append(hits, line)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(hits) == 0 {
|
||||
return ""
|
||||
}
|
||||
if len(hits) > 8 {
|
||||
hits = hits[:8]
|
||||
}
|
||||
return strings.Join(hits, "\n") + "\n"
|
||||
}
|
||||
|
||||
// renderCoverageGapsHits filters analyze:coverage_gaps results to
|
||||
// the intersection with currently-changed symbols. Silent when no
|
||||
// changed symbol carries coverage_pct (i.e. coverage hasn't been
|
||||
// hydrated for this graph) or none of the changed symbols are
|
||||
// undertested. The agent gets a nudge precisely when a coverage
|
||||
// gap was just touched — not a noisy package-wide rollup.
|
||||
func renderCoverageGapsHits(port int, ids []string) string {
|
||||
raw := callServerTool(port, "analyze", map[string]any{
|
||||
"kind": "coverage_gaps",
|
||||
"compact": true,
|
||||
})
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" || strings.HasPrefix(raw, "no coverage gaps") {
|
||||
return ""
|
||||
}
|
||||
|
||||
idSet := make(map[string]bool, len(ids))
|
||||
for _, id := range ids {
|
||||
idSet[id] = true
|
||||
}
|
||||
var hits []string
|
||||
for _, line := range strings.Split(raw, "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
for id := range idSet {
|
||||
if strings.Contains(line, id) {
|
||||
hits = append(hits, line)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(hits) == 0 {
|
||||
return ""
|
||||
}
|
||||
if len(hits) > 8 {
|
||||
hits = hits[:8]
|
||||
}
|
||||
return strings.Join(hits, "\n") + "\n"
|
||||
}
|
||||
|
||||
// renderStaleFlagHits filters analyze:stale_flags results to the
|
||||
// intersection with currently-changed symbols. Silent unless one
|
||||
// of the changed symbols is a flag site (its id appears in the
|
||||
// stale_flags output). Useful for catching a refactor that
|
||||
// inadvertently touched a flag rollout that's been quiet for ages.
|
||||
func renderStaleFlagHits(port int, ids []string) string {
|
||||
raw := callServerTool(port, "analyze", map[string]any{
|
||||
"kind": "stale_flags",
|
||||
"compact": true,
|
||||
})
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" || strings.HasPrefix(raw, "no stale flags") {
|
||||
return ""
|
||||
}
|
||||
|
||||
idSet := make(map[string]bool, len(ids))
|
||||
for _, id := range ids {
|
||||
idSet[id] = true
|
||||
}
|
||||
var hits []string
|
||||
for _, line := range strings.Split(raw, "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
for id := range idSet {
|
||||
if strings.Contains(line, id) {
|
||||
hits = append(hits, line)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(hits) == 0 {
|
||||
return ""
|
||||
}
|
||||
if len(hits) > 8 {
|
||||
hits = hits[:8]
|
||||
}
|
||||
return strings.Join(hits, "\n") + "\n"
|
||||
}
|
||||
|
||||
// renderContractMismatches runs the contracts check and returns a short list
|
||||
// of orphan providers/consumers. Empty when all contracts are matched.
|
||||
func renderContractMismatches(port int) string {
|
||||
raw := callServerTool(port, "contracts", map[string]any{
|
||||
"action": "check",
|
||||
"compact": true,
|
||||
})
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
// The compact summary prefixes "no contract issues" when clean.
|
||||
if strings.HasPrefix(strings.ToLower(raw), "no contract") {
|
||||
return ""
|
||||
}
|
||||
return cappedLines(raw, 10)
|
||||
}
|
||||
Reference in New Issue
Block a user