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
58 changes: 35 additions & 23 deletions .github/workflows/dispatch_shadow_signal.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
name: Long Horizon Shadow Signal

on:
schedule:
# Weekly after Friday US market close. This remains shadow-only and does not
# place trades.
- cron: "30 2 * * 6"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use the Friday market date for scheduled issues

Because this cron fires at 02:30 UTC on Saturday, the new issue step later falls back to dt.date.today() and titles the request with Saturday's date. For the weekly post-Friday-close run, that asks the bridge for an as_of/signal_history date that is one day after the Friday market session, which can misdate the shadow artifact and break point-in-time replay assumptions. Pass the intended market date into post_shadow_signal_request.py or derive it explicitly instead of using the runner's UTC date.

Useful? React with 👍 / 👎.

workflow_dispatch:
inputs:
provider:
Expand Down Expand Up @@ -29,8 +33,9 @@ jobs:
env:
BRIDGE_REPOSITORY: ${{ vars.SELFHOSTED_CODEX_REVIEW_REPOSITORY || 'QuantStrategyLab/CodexAuditBridge' }}
BRIDGE_TASK: long_horizon_signal_shadow
BRIDGE_PROVIDER: ${{ inputs.provider || 'auto' }}
SOURCE_REF: ${{ inputs.source_ref || 'main' }}
BRIDGE_PROVIDER: ${{ github.event.inputs.provider || 'auto' }}
SOURCE_REF: ${{ github.event.inputs.source_ref || 'main' }}
SHADOW_SIGNAL_LABEL: long-horizon-shadow
steps:
- uses: actions/checkout@v6

Expand All @@ -41,6 +46,31 @@ jobs:
- name: Validate existing latest signal if present
run: python scripts/validate_latest_signal.py --allow-missing

- name: Create or update shadow signal issue
id: shadow_issue
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
python scripts/post_shadow_signal_request.py \
--repo "${GITHUB_REPOSITORY}" \
--source-ref "${SOURCE_REF}" \
--provider "${BRIDGE_PROVIDER}" \
--bridge-repository "${BRIDGE_REPOSITORY}" \
--label "${SHADOW_SIGNAL_LABEL}"

- name: Append shadow signal job summary
run: |
{
echo "## Long-horizon shadow signal"
echo ""
echo "- Issue: [${{ steps.shadow_issue.outputs.issue_title }}](${{ steps.shadow_issue.outputs.issue_url }})"
echo "- Issue action: \`${{ steps.shadow_issue.outputs.issue_action }}\`"
echo "- Source ref: \`${SOURCE_REF}\`"
echo "- Provider: \`${BRIDGE_PROVIDER}\`"
echo "- Bridge: \`${BRIDGE_REPOSITORY}\`"
echo "- Mode: \`shadow\`"
} >> "$GITHUB_STEP_SUMMARY"

- name: Detect GitHub App Credentials
id: app_credentials
env:
Expand Down Expand Up @@ -72,6 +102,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
APP_TOKEN: ${{ steps.bridge_app_token.outputs.token }}
CODEX_AUDIT_DISPATCH_TOKEN: ${{ secrets.CODEX_AUDIT_DISPATCH_TOKEN }}
ISSUE_NUMBER: ${{ steps.shadow_issue.outputs.issue_number }}
run: |
python - <<'PY'
import json
Expand Down Expand Up @@ -102,30 +133,11 @@ jobs:
body = response.read().decode("utf-8")
return response.status, json.loads(body) if body else None

issue_body = "\n".join([
"## Long-Horizon Shadow Signal Request",
"",
f"- Source ref: `{os.environ['SOURCE_REF']}`",
"- Mode: `shadow`",
"- Required output: `data/output/latest_signal.json`",
"- AI output must not place orders or change live strategy rules.",
"",
"## Context",
"",
"Use repository examples and any committed context bundles as evidence.",
"If evidence is insufficient, report findings and do not edit artifacts.",
])
_, issue = request(
"POST",
f"https://api.github.com/repos/{repo}/issues",
{"title": "Long-horizon AI shadow signal review", "body": issue_body},
)

dispatch_payload = {
"ref": "main",
"inputs": {
"source_repo": repo,
"issue_number": str(issue["number"]),
"issue_number": os.environ["ISSUE_NUMBER"],
"source_ref": os.environ["SOURCE_REF"],
"mode": "review_and_fix",
"provider": os.environ["BRIDGE_PROVIDER"],
Expand All @@ -141,5 +153,5 @@ jobs:
)
if status not in (201, 204):
raise RuntimeError(f"unexpected dispatch status: {status}")
print(f"Created issue #{issue['number']} and dispatched {bridge_repo}")
print(f"Dispatched {bridge_repo} for issue #{os.environ['ISSUE_NUMBER']}")
PY
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ This repo does not own:

## Operating Model

1. A workflow creates a long-horizon shadow-signal issue.
1. A weekly workflow creates or updates a dated long-horizon shadow-signal issue.
2. The issue is dispatched to `QuantStrategyLab/CodexAuditBridge` with task
`long_horizon_signal_shadow`.
3. `CodexAuditBridge` tries self-hosted Codex first and uses its own OpenAI or
Expand Down Expand Up @@ -57,6 +57,17 @@ Configured non-secret variables:
- `SELFHOSTED_CODEX_REVIEW_PROVIDER=auto`
- `CROSS_REPO_GITHUB_APP_ID=3250578`

## Notification Policy

The GitHub issue created by `.github/workflows/dispatch_shadow_signal.yml` is the
initial operator notification channel. It is labeled `long-horizon-shadow`,
deduplicated by date, and receives the CodexAuditBridge result as comments or a
focused PR.

Do not add Telegram, broker, or runtime plugin notifications at this stage. Those
belong downstream only after the signal graduates from shadow research to a
deterministic plugin contract.

## Local Validation

Validate the example artifact:
Expand Down
4 changes: 4 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ order routing.
- `AiLongHorizonSignalPipelines` stores context examples, schema, validation,
and shadow artifacts.
- `CodexAuditBridge` owns provider routing and API keys.
- GitHub Issues are the first operator notification layer for scheduled shadow
signal runs.
- `QuantStrategyPlugins` may later read promoted artifacts as sidecar context.
- Platform repositories remain unchanged.

Expand All @@ -28,6 +30,8 @@ order routing.
execution mode.
- Re-generating old AI judgments during replay instead of replaying stored
artifacts.
- Sending runtime Telegram or broker-facing notifications directly from this
research repository before a deterministic plugin contract exists.

## Validation Strategy

Expand Down
181 changes: 181 additions & 0 deletions scripts/post_shadow_signal_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
#!/usr/bin/env python3
from __future__ import annotations

import argparse
import datetime as dt
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from typing import Any


DEFAULT_API_URL = "https://api.github.com"
DEFAULT_LABEL = "long-horizon-shadow"


def github_request(method: str, url: str, token: str, payload: dict[str, Any] | None = None) -> Any:
data = None
headers = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {token}",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "ai-long-horizon-shadow-signal",
}
if payload is not None:
data = json.dumps(payload).encode("utf-8")
headers["Content-Type"] = "application/json"
request = urllib.request.Request(url, data=data, headers=headers, method=method)
with urllib.request.urlopen(request, timeout=30) as response:
charset = response.headers.get_content_charset("utf-8")
raw = response.read().decode(charset)
return json.loads(raw) if raw else None


def ensure_label(api_url: str, repo: str, token: str, label: str) -> None:
encoded = urllib.parse.quote(label, safe="")
label_url = f"{api_url}/repos/{repo}/labels/{encoded}"
try:
github_request("GET", label_url, token)
except urllib.error.HTTPError as exc:
if exc.code != 404:
raise
github_request(
"POST",
f"{api_url}/repos/{repo}/labels",
token,
{
"name": label,
"color": "5319E7",
"description": "Long-horizon AI shadow signal request and review",
},
)


def find_existing_issue(api_url: str, repo: str, token: str, label: str, title: str) -> dict[str, Any] | None:
issues = github_request(
"GET",
f"{api_url}/repos/{repo}/issues?state=open&labels={urllib.parse.quote(label)}&per_page=100",
token,
)
return next((issue for issue in issues if issue.get("title") == title), None)


def build_issue_title(as_of_date: str) -> str:
return f"Long-horizon AI shadow signal: {as_of_date}"


def build_issue_body(*, as_of_date: str, source_ref: str, provider: str, bridge_repository: str) -> str:
return "\n".join(
[
"## Long-Horizon Shadow Signal Request",
"",
f"- As of date: `{as_of_date}`",
f"- Source ref: `{source_ref}`",
f"- Bridge repository: `{bridge_repository}`",
f"- Provider: `{provider}`",
"- Mode: `shadow`",
"- Required output: `data/output/latest_signal.json`",
"- Historical copy: `data/output/signal_history/YYYY-MM-DD.json` when evidence is sufficient",
"",
"## Operator Notification",
"",
"This issue is the operator-facing notification for the shadow signal run.",
"CodexAuditBridge should post its review result here and may open a focused PR",
"only for schema-valid shadow artifacts.",
"",
"## Boundaries",
"",
"- AI output must not place orders or change live strategy rules.",
"- Any downstream use must remain advisory until a deterministic policy consumes the artifact.",
"- If evidence is insufficient, report findings and leave artifacts unchanged.",
"",
"## Context",
"",
"Use committed examples, context bundles, and existing shadow artifacts as evidence.",
"Do not infer historical AI signals that were not generated point-in-time.",
]
)


def upsert_issue(
*,
api_url: str,
repo: str,
token: str,
title: str,
body: str,
label: str,
) -> tuple[str, int, str]:
ensure_label(api_url, repo, token, label)
payload = {"title": title, "body": body, "labels": [label]}
existing = find_existing_issue(api_url, repo, token, label, title)
if existing:
updated = github_request("PATCH", f"{api_url}/repos/{repo}/issues/{existing['number']}", token, payload)
return "updated", int(updated["number"]), str(updated["html_url"])
created = github_request("POST", f"{api_url}/repos/{repo}/issues", token, payload)
return "created", int(created["number"]), str(created["html_url"])


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Create or update a long-horizon shadow signal issue.")
parser.add_argument("--repo", required=True)
parser.add_argument("--source-ref", default="main")
parser.add_argument("--provider", default="auto")
parser.add_argument("--bridge-repository", default="QuantStrategyLab/CodexAuditBridge")
parser.add_argument("--as-of-date", default=dt.date.today().isoformat())
parser.add_argument("--label", default=DEFAULT_LABEL)
parser.add_argument("--api-url", default=DEFAULT_API_URL)
return parser.parse_args()


def write_outputs(*, action: str, issue_number: int, issue_url: str, title: str) -> None:
output = os.environ.get("GITHUB_OUTPUT")
if output:
with open(output, "a", encoding="utf-8") as handle:
print(f"issue_action={action}", file=handle)
print(f"issue_number={issue_number}", file=handle)
print(f"issue_url={issue_url}", file=handle)
print(f"issue_title={title}", file=handle)
print(f"issue_action={action}")
print(f"issue_number={issue_number}")
print(f"issue_url={issue_url}")
print(f"issue_title={title}")


def main() -> int:
args = parse_args()
token = os.environ.get("GITHUB_TOKEN")
if not token:
print("GITHUB_TOKEN is required", file=sys.stderr)
return 1

title = build_issue_title(args.as_of_date)
body = build_issue_body(
as_of_date=args.as_of_date,
source_ref=args.source_ref,
provider=args.provider,
bridge_repository=args.bridge_repository,
)
try:
action, issue_number, issue_url = upsert_issue(
api_url=args.api_url.rstrip("/"),
repo=args.repo,
token=token,
title=title,
body=body,
label=args.label,
)
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
print(f"GitHub API request failed: {exc.code} {detail}", file=sys.stderr)
return 1

write_outputs(action=action, issue_number=issue_number, issue_url=issue_url, title=title)
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading