@groundnuty/macf 0.2.34 → 0.2.36
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/dist/.build-info.json +2 -2
- package/dist/cli/claude-sh.d.ts.map +1 -1
- package/dist/cli/claude-sh.js +13 -2
- package/dist/cli/claude-sh.js.map +1 -1
- package/dist/cli/commands/certs.js +3 -3
- package/dist/cli/commands/certs.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +6 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +15 -2
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/plugin-fetcher.d.ts +24 -0
- package/dist/cli/plugin-fetcher.d.ts.map +1 -1
- package/dist/cli/plugin-fetcher.js +61 -1
- package/dist/cli/plugin-fetcher.js.map +1 -1
- package/dist/cli/settings-writer.d.ts +34 -5
- package/dist/cli/settings-writer.d.ts.map +1 -1
- package/dist/cli/settings-writer.js +54 -5
- package/dist/cli/settings-writer.js.map +1 -1
- package/dist/cli/version-resolver.d.ts +2 -5
- package/dist/cli/version-resolver.d.ts.map +1 -1
- package/dist/cli/version-resolver.js +5 -19
- package/dist/cli/version-resolver.js.map +1 -1
- package/dist/reconciler/parse-delivered.d.ts +32 -0
- package/dist/reconciler/parse-delivered.d.ts.map +1 -0
- package/dist/reconciler/parse-delivered.js +18 -0
- package/dist/reconciler/parse-delivered.js.map +1 -0
- package/dist/reconciler/parse-processed.d.ts +57 -0
- package/dist/reconciler/parse-processed.d.ts.map +1 -0
- package/dist/reconciler/parse-processed.js +41 -0
- package/dist/reconciler/parse-processed.js.map +1 -0
- package/dist/reconciler/reconcile.d.ts +99 -0
- package/dist/reconciler/reconcile.d.ts.map +1 -0
- package/dist/reconciler/reconcile.js +75 -0
- package/dist/reconciler/reconcile.js.map +1 -0
- package/dist/reconciler/run.d.ts +3 -0
- package/dist/reconciler/run.d.ts.map +1 -0
- package/dist/reconciler/run.js +184 -0
- package/dist/reconciler/run.js.map +1 -0
- package/package.json +2 -2
- package/plugin/rules/coordination.md +23 -14
- package/plugin/rules/mention-routing-hygiene.md +2 -0
- package/plugin/rules/silent-fallback-hazards.md +49 -10
- package/scripts/check-close-keyword.sh +218 -0
- package/scripts/emit-turn-receipt.sh +81 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# check-close-keyword.sh — Claude Code PreToolUse hook that blocks
|
|
4
|
+
# `gh pr create` / `gh pr edit` invocations whose body / title contains a
|
|
5
|
+
# GitHub auto-close keyword (`close`/`closes`/`closed`/`fix`/`fixes`/`fixed`/
|
|
6
|
+
# `resolve`/`resolves`/`resolved`) adjacent to an issue ref (`#N` or
|
|
7
|
+
# `owner/repo#N`) **filed by another agent**. On merge, such a keyword
|
|
8
|
+
# auto-closes the referenced issue — bypassing reporter-owns-closure
|
|
9
|
+
# (coordination.md §Issue Lifecycle 1). The keyword parser is negation-blind
|
|
10
|
+
# and context-blind (tables, checklists, quotes, code fences don't shield it),
|
|
11
|
+
# so cognitive discipline has proven insufficient (4 self-inflicted recurrences
|
|
12
|
+
# — macf#316/#410/#430). This is the Path-2 structural backstop.
|
|
13
|
+
#
|
|
14
|
+
# Hook contract: JSON on stdin, exit 0 = allow, exit 2 = block (stderr is fed
|
|
15
|
+
# back to Claude as the error). Mirrors check-gh-token.sh / check-mention-
|
|
16
|
+
# routing.sh in shape (wrapper-aware invocation matching, fail-open on parse
|
|
17
|
+
# error so a broken hook never bricks the harness).
|
|
18
|
+
#
|
|
19
|
+
# Discriminator: blocks ONLY when the referenced issue's author is NOT the
|
|
20
|
+
# acting bot. A self-filed `Closes #own` is legitimate and passes. The acting
|
|
21
|
+
# bot login is `${MACF_AGENT_NAME}[bot]` (falls back to $GIT_AUTHOR_NAME, which
|
|
22
|
+
# env.identity sets to `<agent_name>[bot]`).
|
|
23
|
+
#
|
|
24
|
+
# Override: MACF_SKIP_CLOSE_CHECK=1 bypasses (deliberate cross-fix, or the rare
|
|
25
|
+
# case the heuristic mis-resolves authorship).
|
|
26
|
+
#
|
|
27
|
+
# Refs: groundnuty/macf#431 (this hook); coordination.md §Issue Lifecycle 1
|
|
28
|
+
# (the 9 forbidden variants + reporter-owns-closure); silent-fallback-
|
|
29
|
+
# hazards.md Instance 2; #140 / #244+#272 / #270 (sister Path-2 hooks).
|
|
30
|
+
set -euo pipefail
|
|
31
|
+
|
|
32
|
+
# Cheap exit on operator override — no stdin read, no parsing.
|
|
33
|
+
if [[ "${MACF_SKIP_CLOSE_CHECK:-}" == "1" ]]; then
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Read PreToolUse payload. Fall through to allow on parse error — a broken
|
|
38
|
+
# hook must not brick the harness. Same defense-in-depth as the sister hooks.
|
|
39
|
+
INPUT_JSON="$(cat)"
|
|
40
|
+
COMMAND="$(jq -r '.tool_input.command // ""' <<<"$INPUT_JSON" 2>/dev/null || echo "")"
|
|
41
|
+
[[ -z "$COMMAND" ]] && exit 0
|
|
42
|
+
|
|
43
|
+
# Wrapper-aware match for `gh pr create` / `gh pr edit`. Mirrors check-gh-
|
|
44
|
+
# token.sh / check-mention-routing.sh — covers sudo, env VAR=, watch, ionice,
|
|
45
|
+
# setsid, nice, time prefix wrappers + chained-form leadins `;` `|` `&` `(`
|
|
46
|
+
# (subshell) + bare `VAR=val gh ...`. These are the two subcommands whose
|
|
47
|
+
# body/title lands in the merge commit (and thus can auto-close on merge).
|
|
48
|
+
GH_PR_PATTERN='(^|[[:space:];|&(])(sudo[[:space:]]+|env[[:space:]]+([A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*|watch[[:space:]]+|ionice[[:space:]]+|setsid[[:space:]]+|nice[[:space:]]+|time[[:space:]]+|[A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*gh[[:space:]]+pr[[:space:]]+(create|edit)([[:space:]]|$)'
|
|
49
|
+
|
|
50
|
+
# Shell-wrapper bypass: `bash -c "gh pr create ..."` and variants.
|
|
51
|
+
SHELL_C_GH_PR_PATTERN='(^|[[:space:];|&(])(sudo[[:space:]]+|env[[:space:]]+([A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*|[A-Za-z_][A-Za-z_0-9]*=[^[:space:]]*[[:space:]]+)*(bash|sh|zsh)[[:space:]]+(-[a-zA-Z]+[[:space:]]+)*-[a-zA-Z]*c[[:space:]]+[^[:space:]].*gh[[:space:]]+pr[[:space:]]+(create|edit)([[:space:]]|$)'
|
|
52
|
+
|
|
53
|
+
if [[ ! "$COMMAND" =~ $GH_PR_PATTERN ]] && [[ ! "$COMMAND" =~ $SHELL_C_GH_PR_PATTERN ]]; then
|
|
54
|
+
exit 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# ── Acting bot login ─────────────────────────────────────────────────────
|
|
58
|
+
# Prefer MACF_AGENT_NAME (env.identity exports it as the agent stem, e.g.
|
|
59
|
+
# `macf-code-agent`); the bot login is `<stem>[bot]`. Fall back to
|
|
60
|
+
# GIT_AUTHOR_NAME (env.identity sets it to `<stem>[bot]` already). May be empty
|
|
61
|
+
# in a workspace with no identity env — handled conservatively below.
|
|
62
|
+
ACTING_BOT=""
|
|
63
|
+
if [[ -n "${MACF_AGENT_NAME:-}" ]]; then
|
|
64
|
+
# Strip a trailing `[bot]` first so a misconfigured MACF_AGENT_NAME that
|
|
65
|
+
# already carries it doesn't become `<name>[bot][bot]` (which would
|
|
66
|
+
# mis-block self-filed closes). Append exactly once.
|
|
67
|
+
ACTING_BOT="${MACF_AGENT_NAME%"[bot]"}[bot]"
|
|
68
|
+
elif [[ -n "${GIT_AUTHOR_NAME:-}" && "${GIT_AUTHOR_NAME}" == *"[bot]" ]]; then
|
|
69
|
+
ACTING_BOT="${GIT_AUTHOR_NAME}"
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# ── Repo the PR targets (for refs without an owner/repo prefix) ──────────
|
|
73
|
+
# Parse `--repo owner/repo` (or `--repo=owner/repo`) from the command; fall
|
|
74
|
+
# back to gh's default repo resolution for the cwd.
|
|
75
|
+
PR_REPO="$(grep -oE -- '--repo[ =][^[:space:]]+' <<<"$COMMAND" | head -1 | sed -E 's/^--repo[ =]//' | tr -d "\"'" || true)"
|
|
76
|
+
if [[ -z "$PR_REPO" ]]; then
|
|
77
|
+
PR_REPO="$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || true)"
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# ── Build the scan text: the raw command (covers inline --body, --title, and
|
|
81
|
+
# heredoc text) plus the contents of any --body-file (a file path, NOT an
|
|
82
|
+
# inline arg — the bypass most likely to slip a body-only scan). ─────────
|
|
83
|
+
# `-F` is gh's documented short alias for `--body-file` (`gh pr create --help`:
|
|
84
|
+
# `-F, --body-file file`), so normalize the space/`=` forms `-F path` / `-F=path`
|
|
85
|
+
# → `--body-file …` before extraction, else a peer close-keyword in a `-F`-passed
|
|
86
|
+
# file slips the scan (groundnuty/macf#431 review must-fix 1).
|
|
87
|
+
#
|
|
88
|
+
# Known inherent non-guards (backstop, not airtight): `--body-file -` / `-F -`
|
|
89
|
+
# (stdin — unreadable at hook-fire time); the glued short form `-Fpath`; and a
|
|
90
|
+
# path-prefixed binary (`/usr/bin/gh pr create …`, where the leading boundary
|
|
91
|
+
# class doesn't see a bare `gh`). The authorship+merge-time reality still
|
|
92
|
+
# catches the common forms; these are documented so they're known, not silent.
|
|
93
|
+
NORM_CMD="$(sed -E 's/(^|[[:space:]])-F([ =])/\1--body-file\2/g' <<<"$COMMAND")"
|
|
94
|
+
SCAN_TEXT="$COMMAND"
|
|
95
|
+
while IFS= read -r bf; do
|
|
96
|
+
[[ -z "$bf" ]] && continue
|
|
97
|
+
bf="${bf%\"}"; bf="${bf#\"}"; bf="${bf%\'}"; bf="${bf#\'}"
|
|
98
|
+
if [[ -f "$bf" ]]; then
|
|
99
|
+
SCAN_TEXT+=$'\n'"$(cat "$bf" 2>/dev/null || true)"
|
|
100
|
+
fi
|
|
101
|
+
done < <(grep -oE -- '--body-file[ =][^[:space:]]+' <<<"$NORM_CMD" | sed -E 's/^--body-file[ =]//' || true)
|
|
102
|
+
|
|
103
|
+
# ── Find close-keyword → issue-ref adjacencies ───────────────────────────
|
|
104
|
+
# Mirrors GitHub's actual parser: the keyword must be a whole word
|
|
105
|
+
# (word-boundary before it) and immediately followed by only whitespace then
|
|
106
|
+
# the ref — `fix the bug in #1` does NOT auto-close, so it must NOT match.
|
|
107
|
+
# The ref may carry an optional `owner/repo` prefix (cross-repo close, e.g.
|
|
108
|
+
# `closes groundnuty/macf#418` in a devops-toolkit PR — lands in the squash
|
|
109
|
+
# commit + closes the referenced repo's issue).
|
|
110
|
+
KW_ALT='close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved'
|
|
111
|
+
# After the keyword, either a `:` + optional space (`Closes: #5` AND `Closes:#5`)
|
|
112
|
+
# OR required whitespace (`Closes #5`) — but NOT `closes#5` (no separator, which
|
|
113
|
+
# GitHub does not auto-close). The leading boundary `[^A-Za-z]` (or line start)
|
|
114
|
+
# keeps `unfixes #1` / `prefixes #1` from matching.
|
|
115
|
+
ADJ_RE="(^|[^A-Za-z])(${KW_ALT})(:[[:space:]]*|[[:space:]]+)([A-Za-z0-9._-]+/[A-Za-z0-9._-]+)?#[0-9]+"
|
|
116
|
+
|
|
117
|
+
# grep -o yields each match including the optional leading boundary char;
|
|
118
|
+
# strip it so each token is `<kw>[ ](<owner/repo>)?#N`.
|
|
119
|
+
MATCHES="$(grep -oiE "$ADJ_RE" <<<"$SCAN_TEXT" | sed -E 's/^[^A-Za-z]+//' || true)"
|
|
120
|
+
[[ -z "$MATCHES" ]] && exit 0
|
|
121
|
+
|
|
122
|
+
# ── Resolve authorship for each unique ref + classify ────────────────────
|
|
123
|
+
# Returns one of: NOTFOUND (no such issue → safe, no auto-close target),
|
|
124
|
+
# OK:<PR|ISSUE>:<login>, or APIERROR:<msg>.
|
|
125
|
+
resolve_issue() {
|
|
126
|
+
local repo="$1" num="$2" out rc err errfile
|
|
127
|
+
# Single gh call: capture stdout + stderr + rc atomically so the
|
|
128
|
+
# success/404/error classification can't split across two flaky calls.
|
|
129
|
+
errfile="$(mktemp)"
|
|
130
|
+
out="$(GH_PAGER= gh api "repos/${repo}/issues/${num}" 2>"$errfile")"; rc=$?
|
|
131
|
+
err="$(cat "$errfile" 2>/dev/null || true)"; rm -f "$errfile"
|
|
132
|
+
if [[ $rc -ne 0 ]]; then
|
|
133
|
+
if grep -qiE '404|not found' <<<"$err"; then
|
|
134
|
+
echo "NOTFOUND"
|
|
135
|
+
else
|
|
136
|
+
echo "APIERROR:$(tr '\n' ' ' <<<"$err" | cut -c1-120)"
|
|
137
|
+
fi
|
|
138
|
+
return 0
|
|
139
|
+
fi
|
|
140
|
+
local login ispr
|
|
141
|
+
login="$(jq -r '.user.login // ""' <<<"$out")"
|
|
142
|
+
ispr="$(jq -r 'if .pull_request then "PR" else "ISSUE" end' <<<"$out")"
|
|
143
|
+
echo "OK:${ispr}:${login}"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
BLOCKED="" # accumulated human-readable offenders
|
|
147
|
+
SEEN="" # dedup set of "repo#N"
|
|
148
|
+
|
|
149
|
+
while IFS= read -r m; do
|
|
150
|
+
[[ -z "$m" ]] && continue
|
|
151
|
+
num="$(grep -oE '#[0-9]+' <<<"$m" | head -1 | tr -d '#')"
|
|
152
|
+
[[ -z "$num" ]] && continue
|
|
153
|
+
prefix="$(grep -oE '[A-Za-z0-9._-]+/[A-Za-z0-9._-]+#' <<<"$m" | head -1 | sed 's/#$//' || true)"
|
|
154
|
+
kw="$(grep -oiE "^(${KW_ALT})" <<<"$m")"
|
|
155
|
+
target_repo="${prefix:-$PR_REPO}"
|
|
156
|
+
|
|
157
|
+
# Can't determine which repo → can't verify authorship. Conservative block.
|
|
158
|
+
if [[ -z "$target_repo" ]]; then
|
|
159
|
+
BLOCKED+=$'\n'" - \"${kw} #${num}\" → could not determine target repo (pass --repo or run from the repo)"
|
|
160
|
+
continue
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
key="${target_repo}#${num}"
|
|
164
|
+
case " $SEEN " in *" $key "*) continue ;; esac
|
|
165
|
+
SEEN+=" $key"
|
|
166
|
+
|
|
167
|
+
res="$(resolve_issue "$target_repo" "$num")"
|
|
168
|
+
case "$res" in
|
|
169
|
+
NOTFOUND)
|
|
170
|
+
: # no such issue → nothing to auto-close → allow
|
|
171
|
+
;;
|
|
172
|
+
OK:PR:*)
|
|
173
|
+
: # a PR, not an issue → auto-close keywords don't close PRs → allow
|
|
174
|
+
;;
|
|
175
|
+
OK:ISSUE:*)
|
|
176
|
+
login="${res#OK:ISSUE:}"
|
|
177
|
+
if [[ -n "$ACTING_BOT" && "${login,,}" == "${ACTING_BOT,,}" ]]; then
|
|
178
|
+
: # self-filed → legitimate auto-close → allow
|
|
179
|
+
elif [[ -z "$ACTING_BOT" ]]; then
|
|
180
|
+
BLOCKED+=$'\n'" - \"${kw} ${prefix:+${prefix}}#${num}\" → ${target_repo}#${num} filed by ${login}; could not confirm you are the author (no MACF_AGENT_NAME / GIT_AUTHOR_NAME)"
|
|
181
|
+
else
|
|
182
|
+
BLOCKED+=$'\n'" - \"${kw} ${prefix:+${prefix}}#${num}\" → ${target_repo}#${num} filed by ${login} (not you, ${ACTING_BOT})"
|
|
183
|
+
fi
|
|
184
|
+
;;
|
|
185
|
+
APIERROR:*)
|
|
186
|
+
BLOCKED+=$'\n'" - \"${kw} ${prefix:+${prefix}}#${num}\" → could not verify ${target_repo}#${num} author (API: ${res#APIERROR:})"
|
|
187
|
+
;;
|
|
188
|
+
esac
|
|
189
|
+
done <<<"$MATCHES"
|
|
190
|
+
|
|
191
|
+
[[ -z "$BLOCKED" ]] && exit 0
|
|
192
|
+
|
|
193
|
+
cat >&2 <<ERR
|
|
194
|
+
BLOCKED by MACF close-keyword guard: this \`gh pr create/edit\` would auto-close
|
|
195
|
+
an issue you did not file when the PR merges, bypassing reporter-owns-closure
|
|
196
|
+
(coordination.md §Issue Lifecycle 1).
|
|
197
|
+
|
|
198
|
+
Offending close-keyword reference(s):${BLOCKED}
|
|
199
|
+
|
|
200
|
+
GitHub's auto-close parser fires on \`<keyword> #N\` adjacency in a PR body or
|
|
201
|
+
title — negation-blind and context-blind (tables, checklists, quotes, and code
|
|
202
|
+
fences do NOT shield it). On merge it closes the referenced issue before the
|
|
203
|
+
reporter can verify the fix matches their intent.
|
|
204
|
+
|
|
205
|
+
Fix: replace the close-keyword with \`Refs\` (which references without closing):
|
|
206
|
+
Wrong: closes #${num:-N} Right: Refs #${num:-N}
|
|
207
|
+
Wrong: fixes owner/repo#${num:-N} Right: Refs owner/repo#${num:-N}
|
|
208
|
+
|
|
209
|
+
Then the reporter closes it after verifying, per coordination.md.
|
|
210
|
+
|
|
211
|
+
Override (ONLY for a deliberate cross-fix, or your OWN issue the check
|
|
212
|
+
mis-resolved):
|
|
213
|
+
export MACF_SKIP_CLOSE_CHECK=1
|
|
214
|
+
|
|
215
|
+
Refs: groundnuty/macf#431 (this hook); coordination.md §Issue Lifecycle 1
|
|
216
|
+
(the 9 forbidden variants); silent-fallback-hazards.md Instance 2.
|
|
217
|
+
ERR
|
|
218
|
+
exit 2
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# UserPromptSubmit hook — turn-ack receipt for routed prompts
|
|
3
|
+
# (groundnuty/macf#444 Option D, piece 2).
|
|
4
|
+
#
|
|
5
|
+
# When the router injects a prompt it appends a correlation marker
|
|
6
|
+
# `[macf-route:<run_id>:<agent>]` (macf-actions#45, piece 1). When that prompt
|
|
7
|
+
# is submitted AS A TURN, this hook fires and emits a `turn_processed` OTel
|
|
8
|
+
# span carrying (routed_run_id, agent). A reconciler (piece 4) joins the
|
|
9
|
+
# router's delivered-pairs log against these spans: a delivered route with no
|
|
10
|
+
# matching span past the open-threshold = a dropped ping that surfaces
|
|
11
|
+
# structurally (closes the #437 send≠receipt gap). A prompt with NO marker
|
|
12
|
+
# (a typed prompt, a non-routed turn) is a no-op — exit 0, emit nothing.
|
|
13
|
+
#
|
|
14
|
+
# Registered `async: true` (settings.json) so it never adds turn latency. It
|
|
15
|
+
# NEVER blocks the turn: any failure path exits 0 (with a stderr WARN on a
|
|
16
|
+
# genuine emit error — fail-loud per silent-fallback-hazards.md Instance 8).
|
|
17
|
+
#
|
|
18
|
+
# Dependency: a live OTLP endpoint (OTEL_EXPORTER_OTLP_ENDPOINT — populated on
|
|
19
|
+
# substrate by the #418 launcher OTEL block). Absent/unreachable → curl fails
|
|
20
|
+
# → WARN, no span, no crash. So this ships safely before #418's relaunch; it
|
|
21
|
+
# simply no-ops-with-WARN until substrate OTel is live.
|
|
22
|
+
set -uo pipefail
|
|
23
|
+
|
|
24
|
+
INPUT="$(cat)"
|
|
25
|
+
|
|
26
|
+
# Extract the submitted prompt text. jq if available; else scan the raw payload.
|
|
27
|
+
PROMPT=""
|
|
28
|
+
if command -v jq >/dev/null 2>&1; then
|
|
29
|
+
PROMPT="$(printf '%s' "$INPUT" | jq -r '.prompt // empty' 2>/dev/null || true)"
|
|
30
|
+
fi
|
|
31
|
+
[ -z "$PROMPT" ] && PROMPT="$INPUT"
|
|
32
|
+
|
|
33
|
+
# Route-correlation markers: [macf-route:<digits>:<kebab-agent>]. No match → not
|
|
34
|
+
# a routed prompt → no-op. A COALESCED turn (a busy agent processing several
|
|
35
|
+
# queued routed pings in one turn) carries MULTIPLE markers — emit a receipt for
|
|
36
|
+
# EACH distinct one. A receipt then means "the marked prompt reached the agent",
|
|
37
|
+
# not "a distinct turn happened". (Was `head -1`, which under-emitted → the
|
|
38
|
+
# macf#444 reconciler false-flagged the un-receipted markers as drops — #462.)
|
|
39
|
+
MARKERS="$(printf '%s' "$PROMPT" | grep -oE '\[macf-route:[0-9]+:[a-z0-9-]+\]' | sort -u || true)"
|
|
40
|
+
[ -z "$MARKERS" ] && exit 0
|
|
41
|
+
|
|
42
|
+
# Need curl + openssl to emit; absent → degrade silently (no span, no noise).
|
|
43
|
+
command -v curl >/dev/null 2>&1 || exit 0
|
|
44
|
+
command -v openssl >/dev/null 2>&1 || exit 0
|
|
45
|
+
|
|
46
|
+
BASE="${OTEL_EXPORTER_OTLP_ENDPOINT:-http://127.0.0.1:14318}"
|
|
47
|
+
BASE="${BASE%/v1/traces}"
|
|
48
|
+
|
|
49
|
+
# One independent span per distinct marker (own trace/span id + timestamp).
|
|
50
|
+
printf '%s\n' "$MARKERS" | while IFS= read -r MARKER; do
|
|
51
|
+
[ -z "$MARKER" ] && continue
|
|
52
|
+
RUN_ID="$(printf '%s' "$MARKER" | sed -E 's/.*\[macf-route:([0-9]+):([a-z0-9-]+)\].*/\1/')"
|
|
53
|
+
AGENT="$(printf '%s' "$MARKER" | sed -E 's/.*\[macf-route:([0-9]+):([a-z0-9-]+)\].*/\2/')"
|
|
54
|
+
|
|
55
|
+
# Nanosecond epoch. `date +%s%N` is GNU-only (substrate is Linux); on a BSD/mac
|
|
56
|
+
# date that prints a literal `N`, fall back to seconds×1e9.
|
|
57
|
+
NOW="$(date +%s%N 2>/dev/null || true)"
|
|
58
|
+
case "$NOW" in
|
|
59
|
+
*N|'' ) NOW="$(( $(date +%s) * 1000000000 ))" ;;
|
|
60
|
+
esac
|
|
61
|
+
TID="$(openssl rand -hex 16)"
|
|
62
|
+
SID="$(openssl rand -hex 8)"
|
|
63
|
+
|
|
64
|
+
# Identity on resource attrs (matches the collector's resource/paper-dims +
|
|
65
|
+
# the `{resource."gen_ai.agent.name"=…}` TraceQL); correlation on span attrs.
|
|
66
|
+
curl -sf -m 3 -X POST "${BASE}/v1/traces" \
|
|
67
|
+
-H 'Content-Type: application/json' --data-binary @- <<JSON >/dev/null \
|
|
68
|
+
|| echo "WARN: turn_processed span emit failed (endpoint=${BASE}/v1/traces run=${RUN_ID} agent=${AGENT})" >&2
|
|
69
|
+
{"resourceSpans":[{"resource":{"attributes":[
|
|
70
|
+
{"key":"service.name","value":{"stringValue":"${OTEL_SERVICE_NAME:-macf-agent}"}},
|
|
71
|
+
{"key":"gen_ai.agent.name","value":{"stringValue":"${AGENT}"}},
|
|
72
|
+
{"key":"service.namespace","value":{"stringValue":"macf"}}
|
|
73
|
+
]},"scopeSpans":[{"scope":{"name":"macf.hook"},"spans":[{
|
|
74
|
+
"traceId":"${TID}","spanId":"${SID}","name":"turn_processed","kind":1,
|
|
75
|
+
"startTimeUnixNano":"${NOW}","endTimeUnixNano":"${NOW}",
|
|
76
|
+
"attributes":[{"key":"routed_run_id","value":{"stringValue":"${RUN_ID}"}},
|
|
77
|
+
{"key":"agent","value":{"stringValue":"${AGENT}"}}],"status":{"code":1}}]}]}]}
|
|
78
|
+
JSON
|
|
79
|
+
done
|
|
80
|
+
|
|
81
|
+
exit 0
|