Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,13 @@ factory "delivery" {
manual {}

provider "github_issues" {
command = ["bach-trigger-fixture"]
command = ["bach-github-issue-trigger"]
poll_interval = "5m"
config = {
repo = "ApplauseLab/bachkator"
token_env = "GITHUB_TOKEN"
labels = "factory:ship"
}

route {
label = "factory:ship"
Expand Down
1 change: 1 addition & 0 deletions cmd/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- `cmd/bach`: main Bach CLI executable.
- `cmd/bach-docs-gen`: reference documentation generator invoked by the `shell/docs-generate` Bach target.
- `cmd/bach-file-lines`: internal Go file length checker for architecture hygiene.
- `cmd/bach-github-issue-trigger`: GitHub Issues Trigger Provider executable.
- `cmd/bach-lint-cap`: internal lint-report capping helper invoked by the `shell/lint` Bach target.
- Command wiring and production dependency assembly belong in `internal/app` and `internal/cli`, not in executable packages.

Expand Down
23 changes: 23 additions & 0 deletions cmd/bach-github-issue-trigger/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package main

import (
"context"
"fmt"
"os"

"github.com/applauselab/bachkator/internal/githubissuetrigger"
"github.com/applauselab/bachkator/pkg/triggerprotocol"
)

func main() {
provider := githubissuetrigger.New(nil)
if err := triggerprotocol.Serve(
context.Background(),
os.Stdin,
os.Stdout,
provider,
); err != nil {
_, _ = fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
22 changes: 20 additions & 2 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1866,10 +1866,12 @@ factory "delivery" {
manual {}

provider "github_issues" {
command = ["bach-trigger-fixture"]
command = ["bach-github-issue-trigger"]
poll_interval = "5m"
config = {
items_path = ".bach/fixtures/trigger-items.json"
repo = "example/repo"
token_env = "GITHUB_TOKEN"
labels = "factory:ship"
}

route {
Expand Down Expand Up @@ -1899,6 +1901,20 @@ Provider trigger fields:
- `config`: optional map of string keys to string values passed to the provider during handshake and poll.
- `route { label = "...", workflow = "..." }`: optional routing rule. Items with the matching label are routed to the named workflow. When a Factory has multiple workflows, at least one route is required; with a single workflow, omitted routes default to that workflow.

The `bach-github-issue-trigger` provider reads GitHub Issues through the trigger provider protocol. It accepts these `config` keys:

- `repo`: required GitHub repository in `owner/name` form.
- `token_env`: optional environment variable name containing a GitHub token; defaults to `GITHUB_TOKEN` when the provider process receives that variable. `bach factory start` only forwards the variable named by `token_env`, so set `token_env = "GITHUB_TOKEN"` when the daemon should use GitHub token auth. The token value must stay out of the Bachfile.
- `api_url`: optional GitHub API base URL; defaults to `https://api.github.com`.
- `labels`: optional comma-separated GitHub label filter passed to the Issues API.
- `state`: optional issue state, one of `open`, `closed`, or `all`; defaults to `open`.
- `since`: optional RFC3339 timestamp used as the initial cursor when no Bach cursor exists.
- `per_page`: optional GitHub page size from `1` to `100`; defaults to `100`.
- `max_pages`: optional positive page limit per poll; defaults to `5`.
- `priority_label_prefix`: optional label prefix for Work Item priority extraction; defaults to `priority:`.

GitHub Issue labels are preserved as Work Item labels for route matching. Pull requests returned by the GitHub Issues API are ignored. The provider advances its cursor from GitHub `updated_at` timestamps, suppresses issues at or before the stored cursor to avoid duplicate delivery, and treats `priority:critical`, `priority:urgent`, `priority:high`, `priority:normal`, and `priority:low` labels as Bach priorities.

Validation rules:

- Factory and workflow names must start with an ASCII letter, digit, or `_`, and may then contain ASCII letters, digits, `_`, `.`, or `-`.
Expand Down Expand Up @@ -1973,6 +1989,8 @@ resumes, the Work Item fails with a stale-approval message instead of silently i

When a Factory declares provider triggers, `bach factory start` also starts a long-running JSON-RPC session with each provider process. The daemon polls each provider on its configured interval, routes returned items to workflows using labels, and enqueues or updates pending Work Items. If any item in a polled batch fails intake validation, the entire batch is nacked so the provider can redeliver; successfully processed batches are acked and the trigger cursor is advanced. Provider trigger protocol messages conform to `docs/schemas/trigger-provider-v1.schema.json`. Provider intake failures do not fail Work Items that are already queued or active.

Provider subprocesses receive only `PATH`, temp directory variables, and the environment variable named by `config.token_env` when present. Configure token env names explicitly instead of relying on the daemon to pass through the full shell environment.

Use `--json` with any Factory command for machine-readable output. `factory submit` returns
`{"item": <work-item>, "created": true|false}`. `factory list` returns `{"items": [<work-item>, ...]}`.
`factory inspect` and `factory cancel` return a single Work Item object. `factory inspect` includes an
Expand Down
22 changes: 20 additions & 2 deletions docs/reference/36-factory-work-items.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ factory "delivery" {
manual {}

provider "github_issues" {
command = ["bach-trigger-fixture"]
command = ["bach-github-issue-trigger"]
poll_interval = "5m"
config = {
items_path = ".bach/fixtures/trigger-items.json"
repo = "example/repo"
token_env = "GITHUB_TOKEN"
labels = "factory:ship"
}

route {
Expand Down Expand Up @@ -74,6 +76,20 @@ Provider trigger fields:
- `config`: optional map of string keys to string values passed to the provider during handshake and poll.
- `route { label = "...", workflow = "..." }`: optional routing rule. Items with the matching label are routed to the named workflow. When a Factory has multiple workflows, at least one route is required; with a single workflow, omitted routes default to that workflow.

The `bach-github-issue-trigger` provider reads GitHub Issues through the trigger provider protocol. It accepts these `config` keys:

- `repo`: required GitHub repository in `owner/name` form.
- `token_env`: optional environment variable name containing a GitHub token; defaults to `GITHUB_TOKEN` when the provider process receives that variable. `bach factory start` only forwards the variable named by `token_env`, so set `token_env = "GITHUB_TOKEN"` when the daemon should use GitHub token auth. The token value must stay out of the Bachfile.
- `api_url`: optional GitHub API base URL; defaults to `https://api.github.com`.
- `labels`: optional comma-separated GitHub label filter passed to the Issues API.
- `state`: optional issue state, one of `open`, `closed`, or `all`; defaults to `open`.
- `since`: optional RFC3339 timestamp used as the initial cursor when no Bach cursor exists.
- `per_page`: optional GitHub page size from `1` to `100`; defaults to `100`.
- `max_pages`: optional positive page limit per poll; defaults to `5`.
- `priority_label_prefix`: optional label prefix for Work Item priority extraction; defaults to `priority:`.

GitHub Issue labels are preserved as Work Item labels for route matching. Pull requests returned by the GitHub Issues API are ignored. The provider advances its cursor from GitHub `updated_at` timestamps, suppresses issues at or before the stored cursor to avoid duplicate delivery, and treats `priority:critical`, `priority:urgent`, `priority:high`, `priority:normal`, and `priority:low` labels as Bach priorities.

Validation rules:

- Factory and workflow names must start with an ASCII letter, digit, or `_`, and may then contain ASCII letters, digits, `_`, `.`, or `-`.
Expand Down Expand Up @@ -148,6 +164,8 @@ resumes, the Work Item fails with a stale-approval message instead of silently i

When a Factory declares provider triggers, `bach factory start` also starts a long-running JSON-RPC session with each provider process. The daemon polls each provider on its configured interval, routes returned items to workflows using labels, and enqueues or updates pending Work Items. If any item in a polled batch fails intake validation, the entire batch is nacked so the provider can redeliver; successfully processed batches are acked and the trigger cursor is advanced. Provider trigger protocol messages conform to `docs/schemas/trigger-provider-v1.schema.json`. Provider intake failures do not fail Work Items that are already queued or active.

Provider subprocesses receive only `PATH`, temp directory variables, and the environment variable named by `config.token_env` when present. Configure token env names explicitly instead of relying on the daemon to pass through the full shell environment.

Use `--json` with any Factory command for machine-readable output. `factory submit` returns
`{"item": <work-item>, "created": true|false}`. `factory list` returns `{"items": [<work-item>, ...]}`.
`factory inspect` and `factory cancel` return a single Work Item object. `factory inspect` includes an
Expand Down
2 changes: 2 additions & 0 deletions internal/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- `cli`: Cobra command adapters and public CLI contract presentation.
- `config`: Bachfile loading, decoding, validation, and config-time references.
- `dag`, `graph`, `model`, and `target`: target model, dependency graph, graph plugins, and target-kind registration behavior.
- `githubissuetrigger`: GitHub Issues Trigger Provider implementation.
- `evidence`: trusted local evidence path handling, private artifact writes, and Agent Target workspace path resolution.
- `factory`: Factory Work Item service logic, manual queue validation, intake evidence creation, and queue-facing DTOs.
- `factorydaemon`: Factory daemon leases, queue polling, Work Item phase orchestration, and workflow execution.
Expand Down Expand Up @@ -67,6 +68,7 @@
- `internal/factorydaemon/AGENTS.md`: Factory daemon leases, polling, claims, and workflow phase orchestration.
- `internal/git/AGENTS.md`: git evidence helpers.
- `internal/graph/AGENTS.md`: affected-target, explain, provenance, and risk graph analysis.
- `internal/githubissuetrigger/AGENTS.md`: GitHub Issues Trigger Provider implementation.
- `internal/model/AGENTS.md`: shared domain model and target address semantics.
- `internal/plan/AGENTS.md`: Markdown Plan parsing, validation, hashing, graph construction, and pure status derivation.
- `internal/planbatch/AGENTS.md`: multi-Plan batch execution and review-queue orchestration.
Expand Down
1 change: 1 addition & 0 deletions internal/factorydaemon/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- Use Backend client methods for Factory state; do not query private State Store tables directly.
- Run executable work through existing Agent Target, Plan execution, and runner paths instead of creating a parallel execution universe.
- Trigger provider polling runs inside `bach factory start`; there is no standalone trigger poll command.
- Trigger provider subprocesses receive only `PATH`, temp directory variables, and the env variable named by `config.token_env` when present.
- Provider trigger failures are logged and nacked; they do not fail queued or active Work Items.
- Release the daemon lease on shutdown using a fresh timeout context so SIGINT/SIGTERM teardown is not blocked by the canceled signal context.
- Expose tunable queue poll, lease renewal, and lease TTL intervals through the CLI adapter; defaults are 5s poll, 10s renew, 30s TTL.
Expand Down
15 changes: 12 additions & 3 deletions internal/factorydaemon/triggers.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ func (p *triggerPoller) ensureSession(ctx context.Context) error {
cmdCtx, cancel := context.WithCancel(ctx)
cmd := exec.CommandContext(cmdCtx, command[0], command[1:]...)
cmd.Dir = p.service.ConfigProject.Root
cmd.Env = triggerEnvironment()
cmd.Env = triggerEnvironment(p.trigger.Config)
stdin, err := cmd.StdinPipe()
if err != nil {
cancel()
Expand Down Expand Up @@ -334,9 +334,18 @@ func resolveTriggerCommand(command []string) ([]string, error) {
return resolved, nil
}

func triggerEnvironment() []string {
func triggerEnvironment(config map[string]string) []string {
env := []string{}
for _, key := range []string{"PATH", "TMPDIR", "TEMP", "TMP"} {
seen := map[string]struct{}{}
keys := []string{"PATH", "TMPDIR", "TEMP", "TMP"}
if config != nil && config["token_env"] != "" {
keys = append(keys, config["token_env"])
}
for _, key := range keys {
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
if value, ok := os.LookupEnv(key); ok {
env = append(env, key+"="+value)
}
Expand Down
34 changes: 34 additions & 0 deletions internal/factorydaemon/triggers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,31 @@ func TestTriggerPollerNackOnIntakeError(t *testing.T) {
}
}

func TestTriggerEnvironmentDoesNotIncludeDefaultGitHubToken(t *testing.T) {
t.Setenv("PATH", "/bin")
t.Setenv("GITHUB_TOKEN", "secret-token")

env := triggerEnvironment(nil)

if !hasEnvValue(env, "PATH=/bin") {
t.Fatalf("env missing PATH: %v", env)
}
if hasEnvValue(env, "GITHUB_TOKEN=secret-token") {
t.Fatalf("env included implicit GITHUB_TOKEN: %v", env)
}
}

func TestTriggerEnvironmentIncludesConfiguredTokenEnv(t *testing.T) {
t.Setenv("PATH", "/bin")
t.Setenv("BACH_TEST_GITHUB_TOKEN", "secret-token")

env := triggerEnvironment(map[string]string{"token_env": "BACH_TEST_GITHUB_TOKEN"})

if !hasEnvValue(env, "BACH_TEST_GITHUB_TOKEN=secret-token") {
t.Fatalf("env missing configured token: %v", env)
}
}

type fakeTriggerHandler struct {
mu sync.Mutex
result triggerprotocol.PollResult
Expand Down Expand Up @@ -263,3 +288,12 @@ func newTestPoller(
}
return poller, handler, cleanup
}

func hasEnvValue(env []string, want string) bool {
for _, value := range env {
if value == want {
return true
}
}
return false
}
25 changes: 25 additions & 0 deletions internal/githubissuetrigger/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Agent Instructions

## Purpose

`internal/githubissuetrigger/` owns the GitHub Issues Trigger Provider implementation used by the
`bach-github-issue-trigger` executable.

## Local Contracts

- Speak only the public `bach.trigger.v1` protocol through `pkg/triggerprotocol`.
- Keep GitHub API access read-only; do not mutate issues, labels, comments, or repository state.
- Read GitHub tokens from environment variables named by provider config. Never accept token values in
Bachfile config and never log token values.
- Keep Work Item mapping deterministic: stable source IDs, issue labels preserved for Factory routing, cursor
advancement based on GitHub `updated_at` values, and duplicate suppression for issues at or before the
stored cursor.

## Verification

- Use `go run ./cmd/bach run shell/test` after provider changes.
- Use `go run ./cmd/bach run shell/fmt` after Go edits.

## Child DOX Index

- No child `AGENTS.md` files currently exist under `internal/githubissuetrigger/`.
Loading
Loading