Skip to content

feat(workspace): operator Co-authored-by trailer#159

Merged
NeuralEmpowerment merged 2 commits into
mainfrom
feat/plugin-git-with-operator-coauthor
May 10, 2026
Merged

feat(workspace): operator Co-authored-by trailer#159
NeuralEmpowerment merged 2 commits into
mainfrom
feat/plugin-git-with-operator-coauthor

Conversation

@NeuralEmpowerment
Copy link
Copy Markdown
Contributor

@NeuralEmpowerment NeuralEmpowerment commented May 1, 2026

Resolves #158.

Summary

Workspace-shipped prepare-commit-msg hook for operator attribution. Not a Claude Code plugin — the hook lives with the claude-cli provider itself, gets baked into the image, and is installed by the entrypoint.

When SYN_OPERATOR_NAME and SYN_OPERATOR_EMAIL are both set, every workspace commit gets a Co-authored-by: NAME <EMAIL> trailer so PRs the agent opens are attributed to the operator who sponsored the workspace. No-op when either env var is unset (backward compatible).

Layout

providers/workspaces/claude-cli/
└── scripts/
    ├── entrypoint.sh
    └── git-hooks/
        └── prepare-commit-msg     ← new (workspace-shipped, not a plugin)

Changes

  • scripts/git-hooks/prepare-commit-msg (new). Skips merge/squash/template sources to avoid polluting auto-generated messages. Does not skip the commit source — --amend, cherry-pick, and rebase still pick up the trailer when missing; idempotency handles re-adds. Strips CR/LF from operator inputs (defense in depth).
  • Dockerfile: COPY scripts/git-hooks/ → /opt/agentic/git-hooks/.
  • entrypoint.sh: composes hooks from /opt/agentic/git-hooks/ (workspace-shipped) AND plugins/observability/hooks/git/ (event-emission) into $HOME/.git-hooks, then sets core.hooksPath. Two well-documented contributing sources. SYN_OPERATOR_* listed in the env-var header.
  • scripts/build-provider.py: stage_scripts now copies the entire scripts/ tree (was top-level files only) so subdirs like git-hooks/ reach the build context.

Observability plugin: untouched. Its 5 git hooks stay where they are — they emit observability events; that's their job. The new hook sits beside them at runtime, not on top of them.

Tests

New file tests/integration/test_workspace_operator_attribution.py8 tests, all passing against the rebuilt image:

test guards
test_no_env_vars_leaves_message_untouched backward compatibility
test_with_operator_env_appends_trailer happy path
test_only_name_set_is_no_op both env vars required
test_idempotent_when_trailer_already_present no double-append
test_template_source_skipped skip-case
test_merge_source_skipped skip-case
test_amend_commit_does_get_trailer explicit decision NOT to skip commit source
test_newline_in_env_var_does_not_inject_extra_trailer sanitization

just test-workspace → 27/27 pre-existing + 8/8 new = 35 passing (plus one pre-existing flake on test_firecrawl_api_call_inside_container unrelated to this PR).

Sister change in syntropic137

Platform side passes SYN_OPERATOR_* from selfhost settings into the workspace setup phase — separate PR.

Test plan

  • CI: Build Workspace Images and QA pass
  • Confirm with a Syntropic137 selfhost dry-run that an agent commit shows the operator co-author after the platform side ships

Copilot AI review requested due to automatic review settings May 1, 2026 23:47
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a dedicated top-level git plugin to own workspace-wide git lifecycle hooks (previously nested under observability), and adds a new prepare-commit-msg hook to append an operator Co-authored-by: trailer when operator env vars are present.

Changes:

  • Move/centralize workspace git lifecycle hooks into a new plugins/git/ plugin and include it in the Claude CLI workspace image manifest.
  • Update the workspace entrypoint.sh to set core.hooksPath to the new plugin’s hooks directory.
  • Add a prepare-commit-msg hook that appends an operator Co-authored-by: trailer when SYN_OPERATOR_NAME and SYN_OPERATOR_EMAIL are set, and update docs accordingly.

Reviewed changes

Copilot reviewed 9 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
providers/workspaces/claude-cli/scripts/entrypoint.sh Points core.hooksPath at the new plugins/git/hooks location.
providers/workspaces/claude-cli/manifest.yaml Bakes the new git plugin into the workspace image.
plugins/observability/README.md Updates docs to reflect the git hooks relocation and links to the new git plugin README.
plugins/git/hooks/prepare-commit-msg New hook to append operator Co-authored-by: trailer when env vars are set.
plugins/git/hooks/pre-push Emits git_push event via agentic_events before push.
plugins/git/hooks/post-rewrite Emits git_rewrite event via agentic_events after rewrite operations.
plugins/git/hooks/post-merge Emits git_merge event via agentic_events after merges.
plugins/git/hooks/post-commit Emits git_commit event via agentic_events after commits.
plugins/git/hooks/post-checkout Emits git_checkout event via agentic_events after branch checkouts.
plugins/git/hooks/install.py Local/global installer updated to include the expanded hook set including prepare-commit-msg.
plugins/git/README.md New documentation for the git plugin’s observability + attribution responsibilities.
plugins/git/.claude-plugin/plugin.json Declares optional SYN_OPERATOR_* env vars via requires_env for orchestrator forwarding.
Comments suppressed due to low confidence (2)

plugins/git/hooks/install.py:33

  • The module docstring and constants were updated away from "observability", but the CLI help text still says Install observability git hooks (ArgParser description). For consistency and to reduce confusion for local installs, update the CLI description to match the new scope (workspace git hooks / git plugin).
    providers/workspaces/claude-cli/scripts/entrypoint.sh:120
  • The log message and surrounding comment still refer to these as "Git observability hooks", but core.hooksPath now points at the new plugins/git/hooks directory which also contains non-observability behavior (prepare-commit-msg attribution). Update the echo/comment wording to reflect that this is the workspace git hooks plugin (observability + attribution), to avoid confusion when debugging container startup logs.
GIT_HOOKS_DIR="${AGENTIC_PLUGINS_DIR:-/opt/agentic/plugins}/git/hooks"
if [ -d "${GIT_HOOKS_DIR}" ]; then
    git config --global core.hooksPath "${GIT_HOOKS_DIR}"
    echo "[entrypoint] Git observability hooks installed from ${GIT_HOOKS_DIR}"
fi

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +35
#!/bin/sh
# prepare-commit-msg — append operator Co-authored-by trailer.
# Plugin marker for install.py uninstall detection: agentic_events
#
# When the workspace is run by an operator (e.g. via Syntropic137 selfhost),
# SYN_OPERATOR_NAME and SYN_OPERATOR_EMAIL identify the human who sponsored
# the workspace. This hook adds them as a co-author on every agent commit so
# the operator gets attribution on PRs the workspace opens.
#
# Behavior:
# - No-op when either env var is unset (backward compatible).
# - Skipped on merge / squash / commit-template sources to avoid polluting
# auto-generated messages.
# - Idempotent — won't append if the same trailer already exists.
# - Always exits 0 — never blocks a commit on attribution failure.

COMMIT_MSG_FILE="$1"
COMMIT_SOURCE="$2"

case "$COMMIT_SOURCE" in
merge|squash|commit) exit 0 ;;
esac

if [ -z "$SYN_OPERATOR_NAME" ] || [ -z "$SYN_OPERATOR_EMAIL" ]; then
exit 0
fi

TRAILER="Co-authored-by: ${SYN_OPERATOR_NAME} <${SYN_OPERATOR_EMAIL}>"

if grep -qF "$TRAILER" "$COMMIT_MSG_FILE" 2>/dev/null; then
exit 0
fi

printf '\n%s\n' "$TRAILER" >> "$COMMIT_MSG_FILE"
exit 0
Comment thread plugins/git/hooks/prepare-commit-msg Outdated
COMMIT_SOURCE="$2"

case "$COMMIT_SOURCE" in
merge|squash|commit) exit 0 ;;
Comment thread plugins/git/hooks/prepare-commit-msg Outdated
Comment on lines +28 to +29
TRAILER="Co-authored-by: ${SYN_OPERATOR_NAME} <${SYN_OPERATOR_EMAIL}>"

NeuralEmpowerment added a commit that referenced this pull request May 1, 2026
- prepare-commit-msg: also skip 'template' source. Previously the
  case statement claimed to skip template-sourced messages but only
  matched merge/squash/commit. Verified: a template-sourced commit
  now leaves the message unchanged.
- prepare-commit-msg: strip CR/LF from SYN_OPERATOR_NAME/EMAIL before
  building the trailer. Defense in depth — env vars are normally
  well-formed but a stray newline could otherwise inject an extra
  Co-authored-by line. Also exit cleanly when sanitization yields
  an empty value.
- install.py: argparse description no longer says 'observability'
  (matches the new plugin scope: observability + attribution).
- entrypoint.sh: comment + log message updated to 'Workspace git
  hooks' so the plugin's two responsibilities are visible at
  container startup.

Verified against rebuilt image with direct hook invocation:
- merge|squash|template|commit → no trailer
- message → trailer added once
- newline injection in env var → only 1 Co-authored-by line
  (sanitized, malformed text contained inside the name field).

Test coverage suggestion (4th Copilot comment) is a follow-up:
adding a pytest integration test that boots the workspace
container and asserts the hook matrix is non-trivial scope.
@NeuralEmpowerment NeuralEmpowerment force-pushed the feat/plugin-git-with-operator-coauthor branch from 3a512ce to 3c96e77 Compare May 2, 2026 00:41
Copilot AI review requested due to automatic review settings May 2, 2026 00:41
@NeuralEmpowerment NeuralEmpowerment changed the title feat(plugins): new plugins/git with operator Co-authored-by trailer feat(plugins/git): operator Co-authored-by trailer May 2, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +110 to +127
# Multiple plugins can contribute git hooks. We compose them into a single
# runtime directory ($GIT_HOOKS_DIR) and point core.hooksPath at it:
# - observability/hooks/git/ → event-emission (post-commit, pre-push, …)
# - git/hooks/ → other git concerns (prepare-commit-msg, …)
#
# Without this line, git hooks only fire in repos that have their own .git/hooks/
# scripts, which is almost never the case for freshly cloned repos.
GIT_HOOKS_DIR="${AGENTIC_PLUGINS_DIR:-/opt/agentic/plugins}/observability/hooks/git"
if [ -d "${GIT_HOOKS_DIR}" ]; then
# Each plugin owns hook filenames it ships; collisions across plugins are not
# expected today. If two plugins ever ship the same hook name, the second
# symlink wins.
#
# Observability hooks emit JSONL events to stderr; the docker exec stream in
# AgenticEventStreamAdapter uses stderr=STDOUT to capture them alongside
# Claude's stdout, and WorkflowExecutionEngine stores them in TimescaleDB.
GIT_HOOKS_DIR="${HOME}/.git-hooks"
mkdir -p "${GIT_HOOKS_DIR}"

PLUGINS_BASE="${AGENTIC_PLUGINS_DIR:-/opt/agentic/plugins}"
for src_dir in "${PLUGINS_BASE}/observability/hooks/git" "${PLUGINS_BASE}/git/hooks"; do
[ -d "${src_dir}" ] || continue
Comment on lines +110 to +133
# Multiple plugins can contribute git hooks. We compose them into a single
# runtime directory ($GIT_HOOKS_DIR) and point core.hooksPath at it:
# - observability/hooks/git/ → event-emission (post-commit, pre-push, …)
# - git/hooks/ → other git concerns (prepare-commit-msg, …)
#
# Without this line, git hooks only fire in repos that have their own .git/hooks/
# scripts, which is almost never the case for freshly cloned repos.
GIT_HOOKS_DIR="${AGENTIC_PLUGINS_DIR:-/opt/agentic/plugins}/observability/hooks/git"
if [ -d "${GIT_HOOKS_DIR}" ]; then
# Each plugin owns hook filenames it ships; collisions across plugins are not
# expected today. If two plugins ever ship the same hook name, the second
# symlink wins.
#
# Observability hooks emit JSONL events to stderr; the docker exec stream in
# AgenticEventStreamAdapter uses stderr=STDOUT to capture them alongside
# Claude's stdout, and WorkflowExecutionEngine stores them in TimescaleDB.
GIT_HOOKS_DIR="${HOME}/.git-hooks"
mkdir -p "${GIT_HOOKS_DIR}"

PLUGINS_BASE="${AGENTIC_PLUGINS_DIR:-/opt/agentic/plugins}"
for src_dir in "${PLUGINS_BASE}/observability/hooks/git" "${PLUGINS_BASE}/git/hooks"; do
[ -d "${src_dir}" ] || continue
for src in "${src_dir}"/*; do
[ -f "${src}" ] || continue
name=$(basename "${src}")
# Skip non-hook files (installer, docs)
case "${name}" in install.py|*.md|*.txt) continue ;; esac
ln -sf "${src}" "${GIT_HOOKS_DIR}/${name}"
Comment thread plugins/git/hooks/prepare-commit-msg Outdated
Comment on lines +20 to +29
# Skip auto-generated message sources so we don't pollute them.
# merge — message produced by `git merge`
# squash — message produced by `git merge --squash`
# template — user has a commit.template configured
# commit — reusing an existing commit (e.g. `git commit --amend`,
# `git cherry-pick`). The original message already has any
# trailer it should have; idempotency below handles re-adds.
case "$COMMIT_SOURCE" in
merge|squash|template|commit) exit 0 ;;
esac
Comment on lines +31 to +50
if [ -z "$SYN_OPERATOR_NAME" ] || [ -z "$SYN_OPERATOR_EMAIL" ]; then
exit 0
fi

# Strip CR/LF from operator inputs — env vars are normally well-formed but we
# don't want a stray newline in either to inject additional trailers.
NAME=$(printf '%s' "$SYN_OPERATOR_NAME" | tr -d '\r\n')
EMAIL=$(printf '%s' "$SYN_OPERATOR_EMAIL" | tr -d '\r\n')

if [ -z "$NAME" ] || [ -z "$EMAIL" ]; then
exit 0
fi

TRAILER="Co-authored-by: ${NAME} <${EMAIL}>"

if grep -qF "$TRAILER" "$COMMIT_MSG_FILE" 2>/dev/null; then
exit 0
fi

printf '\n%s\n' "$TRAILER" >> "$COMMIT_MSG_FILE"
Workspace-shipped prepare-commit-msg hook for operator attribution.
NOT a Claude Code plugin — the hook lives with the claude-cli provider
itself, gets baked into the image, and is installed by the entrypoint.

When SYN_OPERATOR_NAME and SYN_OPERATOR_EMAIL are both set, every
workspace commit gets a 'Co-authored-by: NAME <EMAIL>' trailer so PRs
the agent opens are attributed to the operator who sponsored the
workspace. No-op when either env var is unset (backward compatible).

Layout:
  providers/workspaces/claude-cli/
    scripts/
      entrypoint.sh
      git-hooks/
        prepare-commit-msg     <- new

Changes:
- New scripts/git-hooks/prepare-commit-msg. Skips merge/squash/template
  message sources to avoid polluting auto-generated messages. Does NOT
  skip 'commit' source so amend / cherry-pick / rebase still pick up
  the trailer when missing; idempotency check handles re-adds. Strips
  CR/LF from operator inputs (defense in depth).
- Dockerfile: COPY scripts/git-hooks/ -> /opt/agentic/git-hooks/.
- entrypoint.sh: compose hooks from /opt/agentic/git-hooks/ AND
  plugins/observability/hooks/git/ into $HOME/.git-hooks, then point
  core.hooksPath there. Two contributing sources, well-documented.
  Also lists SYN_OPERATOR_* in the env var header.
- scripts/build-provider.py: stage_scripts now copies the entire
  scripts/ tree (was top-level files only) so subdirs like git-hooks/
  reach the build context.
- New tests/integration/test_workspace_operator_attribution.py with
  8 tests covering: no-env / with-env / one-env-only / idempotency /
  template-skip / merge-skip / amend-still-gets-trailer / newline-
  injection-sanitized.

Resolves #158.

Verification (rebuilt 2.1.126 image):
  - 8/8 new operator-attribution tests pass.
  - 27/27 pre-existing integration tests pass.
  - Composed hooks layout in container:
      $HOME/.git-hooks/
        post-checkout      -> plugins/observability/hooks/git/post-checkout
        post-commit        -> plugins/observability/hooks/git/post-commit
        post-merge         -> plugins/observability/hooks/git/post-merge
        post-rewrite       -> plugins/observability/hooks/git/post-rewrite
        pre-push           -> plugins/observability/hooks/git/pre-push
        prepare-commit-msg -> /opt/agentic/git-hooks/prepare-commit-msg
  - Observability git_commit events still emit on stderr.
@NeuralEmpowerment NeuralEmpowerment force-pushed the feat/plugin-git-with-operator-coauthor branch from dc05818 to c041e38 Compare May 5, 2026 20:20
@NeuralEmpowerment NeuralEmpowerment changed the title feat(plugins/git): operator Co-authored-by trailer feat(workspace): operator Co-authored-by trailer May 5, 2026
Copilot AI review requested due to automatic review settings May 5, 2026 20:25
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@NeuralEmpowerment NeuralEmpowerment merged commit f0161a8 into main May 10, 2026
21 checks passed
@NeuralEmpowerment NeuralEmpowerment deleted the feat/plugin-git-with-operator-coauthor branch May 10, 2026 20:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(workspace): operator Co-authored-by trailer on every workspace commit

2 participants