feat: Sentry SDK + Linear GraphQL sync bridge - #16
timerloggedout-spec wants to merge 13 commits into
Conversation
- Add archwiz/sentry_init.py with official DSN, tracing, profiling, logs - Upgrade archwiz/linear_sync.py from mock to Linear GraphQL API (LINEAR_API_KEY) - Document setup in docs/SENTRY_LINEAR.md - Wire optional Sentry capture into linear_sync error paths Implements observability for Termux monorepo; Linear connector now actionable.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| for task in tasks: | ||
| task_id = str(task.get("id") or task.get("identifier") or "") | ||
| title = task.get("title") or task.get("name") or task_id | ||
| is_done = any(task_id and task_id in line for line in done_lines) |
There was a problem hiding this comment.
🔴 Task status matching can mark the wrong issues as finished
A task is considered finished whenever its id appears anywhere inside any line of the completion file (task_id in line at archwiz/linear_sync.py:202), so short or prefix-sharing ids match unrelated text and those issues get pushed to Done in the tracker.
Impact: Issues that are still open can be silently flipped to Done in Linear.
Substring containment instead of token/identifier matching
get_done_tasks() (archwiz/linear_sync.py:73-81) returns raw lines of taDone.md. sync_to_linear then does any(task_id and task_id in line for line in done_lines). With ids like 5, TER-5, or task-1, any line containing TER-50, 15, or prose mentioning the id counts as done. The resulting is_done drives a live update_issue_state call at archwiz/linear_sync.py:224, so a false positive writes a Done state to a real Linear issue.
A safer check would tokenize the line (e.g. regex word-boundary match on the identifier) before declaring completion.
| is_done = any(task_id and task_id in line for line in done_lines) | |
| is_done = bool(task_id) and any( | |
| re.search(rf"(?<![\w-]){re.escape(task_id)}(?![\w-])", line) | |
| for line in done_lines | |
| ) |
Was this helpful? React with 👍 or 👎 to provide feedback.
| data = linear_query(api_key, q, {"name": team_name}) | ||
| teams = data.get("teams", {}).get("nodes", []) | ||
| if not teams: | ||
| q2 = """ | ||
| query { | ||
| teams { | ||
| nodes { id name key states { nodes { id name type } } } | ||
| } | ||
| } | ||
| """ | ||
| data = linear_query(api_key, q2) | ||
| teams = data.get("teams", {}).get("nodes", []) | ||
| mapping: Dict[str, str] = {} | ||
| for t in teams: | ||
| for s in t.get("states", {}).get("nodes", []): | ||
| mapping[s["name"].lower()] = s["id"] | ||
| mapping[s["type"].lower()] = s["id"] | ||
| return mapping |
There was a problem hiding this comment.
🟡 Status lookup can pick states belonging to unrelated teams
When the configured team cannot be found, the fallback collects statuses from every team in the workspace and merges them into one lookup table (archwiz/linear_sync.py:157-171), so issues get updated with a status that belongs to some other team and the update is rejected or wrong.
Impact: Syncing silently fails or writes an unintended status for issues outside the intended team.
Unfiltered team fallback plus number-only issue fallback
list_team_states first filters teams(filter: { name: { eq: $name } }) using TEAM_KEY (default Termux-monorepo_linear, which looks like a team key, not a name, so the filter likely returns nothing). The fallback query fetches all teams and the loop at archwiz/linear_sync.py:166-171 flattens every team's states into a single name/type -> id map, with later teams overwriting earlier ones. The resulting done_state_id / todo_state_id (archwiz/linear_sync.py:196-197) may belong to a different team than the issue being updated; Linear rejects a stateId from another team, so update_issue_state errors out for every task.
Related: the fallback in find_issue_by_identifier (archwiz/linear_sync.py:117-126) filters only on issue number, which is not unique across teams and can return an issue from an unrelated team.
Prompt for agents
archwiz/linear_sync.py list_team_states() resolves Linear workflow states in a team-agnostic way. The primary query filters teams by name using LINEAR_TEAM (default 'Termux-monorepo_linear', which appears to be a team key rather than a name), so it will usually return no teams and fall through to the fallback that lists ALL teams and merges every team's states into a single name/type -> id map (later teams silently overwrite earlier ones). Since Linear requires the stateId passed to issueUpdate to belong to the issue's own team, the resulting update calls can target states from the wrong team and fail (or apply an unexpected state). Consider matching teams by both key and name, and resolving states per-issue (e.g. from the issue's team) instead of building a single global map. Same concern applies to the number-only issue fallback in find_issue_by_identifier(), where issue numbers are not unique across teams.
Was this helpful? React with 👍 or 👎 to provide feedback.
| logging_integration = LoggingIntegration( | ||
| level=None, # capture all levels as breadcrumbs | ||
| event_level=None, # do not auto-send log records as events | ||
| ) |
There was a problem hiding this comment.
🟡 Log breadcrumbs are disabled despite the code claiming to capture them
The error reporting setup turns off log capture entirely by passing a disabling value (level=None at archwiz/sentry_init.py:53) while the accompanying comment says all levels are captured, so reports arrive without any preceding log context.
Impact: Error reports lose the log history that would explain what happened before the failure.
Sentry LoggingIntegration semantics
In the Sentry Python SDK, LoggingIntegration(level=None) explicitly disables breadcrumb recording for log records; event_level=None disables auto-generated events. The intent expressed by the inline comment ("capture all levels as breadcrumbs") requires level=logging.DEBUG (or leaving the default logging.INFO) while keeping event_level=None.
Was this helpful? React with 👍 or 👎 to provide feedback.
| #!/usr/bin/env python3 | ||
| """ | ||
| Linear Sync Bridge for ArchWiz. | ||
|
|
||
| Syncs local task status (taDone.md / master_tasks.json) to Linear.app | ||
| using the Linear GraphQL API when LINEAR_API_KEY is set. | ||
|
|
||
| Falls back to dry-run / report mode when the key is absent so the bridge | ||
| remains usable for agents and CI without secrets. | ||
|
|
||
| Requires: requests (stdlib urllib used as fallback) | ||
| Optional: Sentry via archwiz.sentry_init | ||
| """ |
There was a problem hiding this comment.
🟡 New work added without the required proposal item row
The change introduces new tooling (Sentry bootstrap and Linear bridge) without adding a corresponding row to any active proposal's item list, which the repository's agent rules require before new work is implemented.
Impact: Work is untracked in the proposal process, breaking the repo's consensus/close bookkeeping.
AGENTS.md hard rule
AGENTS.md states: "Do not invent work outside docs/proposals/active/<id>/ITEMS.md — add a row first and a Linear TER-* issue." The PR cites Implements: TER-14 but no docs/proposals/active/*/ITEMS.md file is touched, and no existing item mentions Sentry or the Linear bridge.
Was this helpful? React with 👍 or 👎 to provide feedback.
| dsn = dsn or os.environ.get("SENTRY_DSN") or DEFAULT_DSN | ||
| if not dsn: | ||
| return False |
There was a problem hiding this comment.
🔍 Empty SENTRY_DSN cannot disable Sentry
dsn or os.environ.get("SENTRY_DSN") or DEFAULT_DSN means setting SENTRY_DSN="" (the conventional way to turn telemetry off) still falls through to the baked-in DSN, so events are always shipped to the hardcoded project. Consider treating a present-but-empty SENTRY_DSN (or an explicit SENTRY_DISABLED/ARCHWIZ_ENV guard) as "disabled", which also matters for CI runs where send_default_pii=True and 100% tracing/profiling are enabled by default.
Was this helpful? React with 👍 or 👎 to provide feedback.
| def _http_post(url: str, headers: Dict[str, str], body: dict) -> dict: | ||
| """Minimal HTTP POST with requests or urllib.""" | ||
| try: | ||
| import requests | ||
| r = requests.post(url, headers=headers, json=body, timeout=30) | ||
| r.raise_for_status() | ||
| return r.json() | ||
| except ImportError: | ||
| import urllib.request | ||
| data = json.dumps(body).encode("utf-8") | ||
| req = urllib.request.Request(url, data=data, headers=headers, method="POST") | ||
| with urllib.request.urlopen(req, timeout=30) as resp: | ||
| return json.loads(resp.read().decode("utf-8")) |
There was a problem hiding this comment.
📝 Info: requests fallback path only triggers on ImportError, and HTTP errors hide GraphQL error bodies
The except ImportError also catches an ImportError raised from inside requests.post internals, which would then re-issue the request via urllib (duplicate POST/mutation). Additionally r.raise_for_status() discards the JSON body; Linear returns useful GraphQL error payloads with 4xx responses, so linear_query's errors handling at archwiz/linear_sync.py:91-92 never sees them and callers only get a bare HTTP status.
Was this helpful? React with 👍 or 👎 to provide feedback.
| try: | ||
| data = linear_query(api_key, q, {"id": identifier}) | ||
| return data.get("issue") | ||
| except Exception: | ||
| # fallback: search by number | ||
| try: | ||
| num = int(identifier.split("-")[-1]) | ||
| except ValueError: | ||
| return None | ||
| q2 = """ | ||
| query($filter: IssueFilter) { | ||
| issues(filter: $filter, first: 1) { | ||
| nodes { id identifier title state { id name type } } | ||
| } | ||
| } | ||
| """ | ||
| data = linear_query(api_key, q2, {"filter": {"number": {"eq": num}}}) | ||
| nodes = data.get("issues", {}).get("nodes", []) | ||
| return nodes[0] if nodes else None |
There was a problem hiding this comment.
📝 Info: Broad except hides real failures in issue lookup fallback
except Exception around the primary issue(id:) query means transport failures, auth errors and rate limits all silently degrade to the number-based search, which then issues a second API call whose failure propagates to the caller. Narrowing the fallback to "issue not found" conditions would make auth/network problems visible instead of producing confusing secondary errors.
Was this helpful? React with 👍 or 👎 to provide feedback.
| current = (issue.get("state") or {}).get("name", "").lower() | ||
| if (is_done and current in ("done", "completed")) or ( | ||
| not is_done and current in ("todo", "backlog", "unstarted") | ||
| ): | ||
| print(" (already in correct state)") | ||
| continue |
There was a problem hiding this comment.
📝 Info: Hardcoded state-name list for the no-op check
The "already in correct state" short-circuit compares the issue's current state name against a fixed list (done, completed, todo, backlog, unstarted). Workspaces with custom state names (e.g. "Shipped", "Ready") will never match, so the script issues a redundant issueUpdate on every run. Comparing the issue's current state id against the resolved target_state would be robust.
Was this helpful? React with 👍 or 👎 to provide feedback.
| # Add root to path for config import | ||
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||
| from archwiz.config import ARCHWIZ_DIR, WORKSPACE_DIR, LOG_DIR |
There was a problem hiding this comment.
📝 Info: Unused import and sys.path mutation
LOG_DIR is imported but never used, and the sys.path.insert at line 23 is only needed for direct script execution; when the module is imported as archwiz.linear_sync it prepends the repo root to sys.path as a side effect for the whole process, which can shadow third-party modules with same-named top-level files in the repo root (there are many, e.g. my_script.py, mapper_graph.py).
Was this helpful? React with 👍 or 👎 to provide feedback.
| except Exception: | ||
| # fallback: search by number | ||
| try: | ||
| num = int(identifier.split("-")[-1]) | ||
| except ValueError: | ||
| return None | ||
| q2 = """ | ||
| query($filter: IssueFilter) { | ||
| issues(filter: $filter, first: 1) { | ||
| nodes { id identifier title state { id name type } } | ||
| } | ||
| } | ||
| """ | ||
| data = linear_query(api_key, q2, {"filter": {"number": {"eq": num}}}) | ||
| nodes = data.get("issues", {}).get("nodes", []) | ||
| return nodes[0] if nodes else None |
There was a problem hiding this comment.
🔴 Task sync can overwrite the status of an unrelated Linear ticket
When the direct ticket lookup fails, the sync falls back to searching by the trailing number of the local task id ({"number": {"eq": num}} at archwiz/linear_sync.py:124) without restricting the search to the configured team, so a completely unrelated ticket can be picked and have its status changed.
Impact: Tickets in other Linear teams can be silently flipped to Done or Todo by a routine sync.
Mechanism: unscoped number filter combined with local task ids
sync_to_linear passes local task ids (e.g. arch-016, see archwiz/taDone.md) into find_issue_by_identifier (archwiz/linear_sync.py:210). The primary issue(id: ...) lookup will fail for such non-Linear identifiers, and the broad except Exception at archwiz/linear_sync.py:111 falls through to the number search. int("arch-016".split("-")[-1]) yields 16, and the IssueFilter has no team constraint, so issues(filter: ..., first: 1) returns whichever issue numbered 16 the API returns first — potentially from any team the API key can access. Its id is then passed to update_issue_state at archwiz/linear_sync.py:224.
Prompt for agents
In archwiz/linear_sync.py, find_issue_by_identifier falls back to searching Linear issues by number when the identifier lookup fails. The fallback filter is not scoped to a team and the identifier being passed in is a local task id (e.g. 'arch-016'), so an arbitrary unrelated issue can be matched and later mutated by update_issue_state. Consider validating that the local task id actually looks like a Linear identifier (TEAMKEY-NUMBER) before attempting any lookup, and scoping the number-based fallback with a team filter (team key derived from the identifier or from LINEAR_TEAM). Also consider not swallowing transport errors into the fallback path, so a network error does not trigger a fuzzy number search.
Was this helpful? React with 👍 or 👎 to provide feedback.
| def get_done_tasks() -> List[str]: | ||
| tadone = WORKSPACE_DIR / "termux-multi-agent" / "taDone.md" | ||
| if tadone.exists(): | ||
| return tadone.read_text(encoding="utf-8").splitlines() | ||
| # also check archwiz/taDone.md symlink target | ||
| alt = ARCHWIZ_DIR / "taDone.md" | ||
| if alt.exists(): | ||
| return alt.read_text(encoding="utf-8").splitlines() | ||
| return [] |
There was a problem hiding this comment.
🔴 Completed work recorded in other task logs is treated as unfinished and reverted
Only one completion log location is consulted (WORKSPACE_DIR / "termux-multi-agent" / "taDone.md" at archwiz/linear_sync.py:74-81) and the function returns as soon as it finds it, so work recorded elsewhere counts as unfinished and its ticket is pushed back to Todo.
Impact: Finished tickets in Linear can be silently moved back to Todo/Backlog.
Mechanism: incomplete completion-log coverage plus unconditional Todo push
The repo maintains several completion logs — archwiz/archivist.py:20-26 lists workspace/taDone.md, workspace/deepcli/taDone.md, workspace/deepcli-tui/taDone.md, workspace/termux-multi-agent/taDone.md, workspace/harmony_hub/taDone.md. get_done_tasks reads only the termux-multi-agent file (and, only if that is missing, archwiz/taDone.md). Any task completed and logged in one of the other files gets is_done = False at archwiz/linear_sync.py:202, and the loop then actively sets the Linear issue to the Todo/unstarted/backlog state at archwiz/linear_sync.py:214-224, undoing a genuine Done state.
Prompt for agents
archwiz/linear_sync.py get_done_tasks only reads workspace/termux-multi-agent/taDone.md (with archwiz/taDone.md as a fallback), while the rest of the codebase (see TASQUE_FILES in archwiz/archivist.py) treats five taDone.md locations as authoritative. Because sync_to_linear actively pushes any task not found in the done list back to the Todo state, missing a completion log causes real Done issues in Linear to be reverted. Consider aggregating lines from all known taDone locations instead of returning after the first hit, and/or only transitioning issues forward to Done rather than forcing not-done issues back to Todo.
Was this helpful? React with 👍 or 👎 to provide feedback.
| def get_tasks() -> List[Dict[str, Any]]: | ||
| master_tasks = ARCHWIZ_DIR / "master_tasks.json" | ||
| if not master_tasks.exists(): | ||
| return [] |
There was a problem hiding this comment.
🔍 Task source path diverges from the rest of ArchWiz
Every other module reads tasks from HOME/workspace/llm_map/master_tasks.json (archwiz/archivist.py:13, archwiz/autonomous_runner.py:8, archwiz/auto_repair.py:9), while this module reads ARCHWIZ_DIR / "master_tasks.json", which in the checkout is a symlink to the device-absolute Termux path. Off-device (CI, containers) the symlink dangles, exists() returns False, and the sync silently reports zero tasks rather than erroring. Using the same canonical WORKSPACE_DIR / "llm_map" / "master_tasks.json" path would avoid the device dependency.
Was this helpful? React with 👍 or 👎 to provide feedback.
| def init_sentry( | ||
| dsn: Optional[str] = None, | ||
| *, | ||
| project: Optional[str] = None, | ||
| traces_sample_rate: float = 1.0, | ||
| profile_session_sample_rate: float = 1.0, | ||
| profile_lifecycle: str = "trace", | ||
| enable_logs: bool = True, | ||
| send_default_pii: bool = True, | ||
| ) -> bool: |
There was a problem hiding this comment.
📝 Info: PII and 100% tracing/profiling enabled by default
Defaults of send_default_pii=True, traces_sample_rate=1.0 and profile_session_sample_rate=1.0 apply to every process that imports this module (importing archwiz.linear_sync alone calls init_sentry() at import time, archwiz/linear_sync.py:28). Full-rate profiling on a Termux device is a meaningful CPU/battery cost, and default PII means request headers/user data are shipped to Sentry unless the caller overrides. Consider env-driven sample rates and PII off by default.
Was this helpful? React with 👍 or 👎 to provide feedback.
| try: | ||
| from archwiz.sentry_init import init_sentry, capture_exception, capture_message | ||
| init_sentry() | ||
| except Exception: | ||
| def capture_exception(exc): # type: ignore | ||
| pass | ||
| def capture_message(msg, level="info"): # type: ignore | ||
| pass |
There was a problem hiding this comment.
📝 Info: Import-time side effect in the sync module
init_sentry() runs at module import, so merely importing archwiz.linear_sync (e.g. from the dashboard menu or tests) starts a Sentry client with tracing and profiling. Deferring initialization to sync_to_linear() / __main__ would keep imports side-effect free.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
Linear agent protocol landed (Implements: TER-14)
Agents must:
MCP: |
| def team_states() -> Dict[str, str]: | ||
| q = """ | ||
| query { | ||
| teams { | ||
| nodes { | ||
| name | ||
| states { nodes { id name type } } | ||
| } | ||
| } | ||
| } | ||
| """ | ||
| data = gql(q) | ||
| out: Dict[str, str] = {} | ||
| for t in data.get("teams", {}).get("nodes", []): | ||
| for s in t.get("states", {}).get("nodes", []): | ||
| out[s["name"].lower()] = s["id"] | ||
| out[s["type"].lower()] = s["id"] | ||
| return out |
There was a problem hiding this comment.
🟡 Status updates can use a workflow state belonging to a different team
Workflow state ids are collected from every team in the workspace and stored under the same names (team_states() at archwiz/linear_client.py:108-125), ignoring the configured team, so a start/done command may apply another team's state and be rejected or land wrongly.
Impact: The start/done commands can fail or move an issue into a state that does not belong to its team.
Mechanism: unfiltered teams query with name/type collisions
team_states() queries teams { nodes { name states { nodes { id name type } } } } with no filter and writes out[name.lower()] = id and out[type.lower()] = id for every team, so later teams overwrite earlier ones. cmd_start (archwiz/linear_client.py:206-211) and cmd_done (archwiz/linear_client.py:221-226) then pass whatever id survived to issueUpdate for an issue that may belong to a different team. Note TEAM_NAME (archwiz/linear_client.py:32) is defined but only used in create_issue. archwiz/linear_sync.py:142-171 has the same problem in its all-teams fallback path.
Filter the states query by the issue's team (or by TEAM_NAME) before building the mapping.
Prompt for agents
archwiz/linear_client.py team_states() gathers workflow states from all teams into one flat name->id map, with later teams silently overwriting earlier entries, and ignores the TEAM_NAME/LINEAR_TEAM configuration that is otherwise honoured in create_issue. cmd_start/cmd_done then send a possibly foreign team's stateId to issueUpdate. Scope the states lookup to the relevant team - either filter teams by TEAM_NAME, or resolve the team from the issue being updated (issue { team { id } }) and fetch that team's states. archwiz/linear_sync.py list_team_states has the same all-teams fallback and should be scoped similarly.
Was this helpful? React with 👍 or 👎 to provide feedback.
| is_done = any(task_id and task_id in line for line in done_lines) | ||
| status = "DONE" if is_done else "TODO" | ||
| print(f" [{task_id}] {title[:60]} -> {status}") |
There was a problem hiding this comment.
📝 Info: Done detection uses substring matching against the whole done-log
task_id in line treats any occurrence of the id anywhere in a taDone.md line as completion. Short or generic ids (e.g. 1, test) will match unrelated log lines and mark tasks Done, which then drives a live state write. Anchoring the match (e.g. f"{task_id}:" or a regex on the id token) would make the mapping deterministic.
Was this helpful? React with 👍 or 👎 to provide feedback.
| try: | ||
| from archwiz.sentry_init import init_sentry, capture_exception, capture_message | ||
| init_sentry() | ||
| except Exception: | ||
| def capture_exception(exc): # type: ignore | ||
| pass | ||
| def capture_message(msg, level="info"): # type: ignore | ||
| pass |
There was a problem hiding this comment.
📝 Info: Sentry helper fallbacks silently disable reporting if init throws
The try block covers both the import and the init_sentry() call, so if the import succeeded but init raised (see ANALYSIS-0001), the except replaces the working capture_exception/capture_message with no-ops for the whole run — errors reported later in sync_to_linear (archwiz/linear_sync.py:228-230) are dropped without any indication. Separating the import from the init call would preserve reporting. Same shape at archwiz/linear_client.py:24-29.
Was this helpful? React with 👍 or 👎 to provide feedback.
| TEAM_NAME = os.environ.get("LINEAR_TEAM", "Termux-monorepo_linear") | ||
| PROJECT_NAME = os.environ.get("LINEAR_PROJECT", "termux-monorepo hardening") |
There was a problem hiding this comment.
📝 Info: Unused configuration constants in the new modules
PROJECT_NAME is read from the environment but never used — issues created via create_issue (archwiz/linear_client.py:153-185) are not attached to the configured project, so the documented "Project: termux-monorepo hardening" convention in docs/LINEAR-AGENT-PROTOCOL.md:86-88 is not enforced by the CLI. Similarly LOG_DIR imported at archwiz/linear_sync.py:24 is unused.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
Ack TER-14 comment (Operator):
Reflected in repo (this PR):
|
| global _initialized | ||
| if _initialized: | ||
| return True | ||
|
|
||
| resolved = _resolve_dsn(dsn, project) | ||
| if not resolved: | ||
| return False |
There was a problem hiding this comment.
🟡 Web service errors can be reported to the wrong monitoring project
The setup call exits early whenever monitoring was already switched on (if _initialized: return True at archwiz/sentry_init.py:64-65), so a later request for the web-service project silently keeps the first project's destination.
Impact: aiohttp service errors and traces can be filed under the generic Python project, making them hard to find.
Module-level init in the Linear helpers pre-empts project selection
archwiz/linear_client.py:25-26 and archwiz/linear_sync.py:27-28 call init_sentry() at import time with the default python DSN. Any process that imports either module and later calls init_sentry(project="aiohttp") (the documented pattern in docs/SENTRY_LINEAR.md:88) gets True back without re-initializing, so DSN_AIOHTTP is never used.
A guard that also tracks the resolved DSN — re-initializing when a different DSN is requested — would make the documented per-project selection work.
Prompt for agents
archwiz/sentry_init.py init_sentry() short-circuits on a global _initialized flag, so a second call requesting a different project DSN is a no-op. Because archwiz/linear_client.py and archwiz/linear_sync.py call init_sentry() at import time with the default python DSN, an aiohttp app that later calls init_sentry(project="aiohttp") never gets the aiohttp DSN. Consider remembering the resolved DSN and re-initializing (or at least warning) when a different DSN is requested.
Was this helpful? React with 👍 or 👎 to provide feedback.
| sentry_sdk.init( | ||
| dsn=resolved, | ||
| send_default_pii=send_default_pii, | ||
| enable_logs=enable_logs, | ||
| traces_sample_rate=traces_sample_rate, | ||
| profile_session_sample_rate=profile_session_sample_rate, | ||
| profile_lifecycle=profile_lifecycle, | ||
| integrations=[logging_integration], | ||
| environment=os.environ.get("ARCHWIZ_ENV", "local"), | ||
| release=os.environ.get("SENTRY_RELEASE"), | ||
| ) |
There was a problem hiding this comment.
🔍 Sentry init kwargs assume a recent sentry-sdk and no dependency is declared
enable_logs, profile_session_sample_rate and profile_lifecycle are only accepted by newer sentry-sdk releases; older installs will raise TypeError from sentry_sdk.init. In linear_client/linear_sync this is swallowed by the import-time try/except, but a direct caller (e.g. an aiohttp service following docs/SENTRY_LINEAR.md) would crash at startup. sentry-sdk is also not added to requirements-base.txt, so the version is unconstrained. A minimum-version pin would make the failure mode explicit.
Was this helpful? React with 👍 or 👎 to provide feedback.
| def get_tasks() -> List[Dict[str, Any]]: | ||
| master_tasks = ARCHWIZ_DIR / "master_tasks.json" | ||
| if not master_tasks.exists(): | ||
| return [] | ||
| try: | ||
| with open(master_tasks, encoding="utf-8") as f: | ||
| data = json.load(f) | ||
| except (json.JSONDecodeError, OSError) as exc: | ||
| print(f"Failed to read {master_tasks}: {exc}", file=sys.stderr) | ||
| capture_exception(exc) | ||
| return [] | ||
| if isinstance(data, dict): | ||
| data = data.get("tasks", []) | ||
| if not isinstance(data, list): | ||
| print(f"Unexpected task format in {master_tasks}", file=sys.stderr) | ||
| return [] | ||
| return [t for t in data if isinstance(t, dict)] |
There was a problem hiding this comment.
📝 Info: Bridge reads an empty master_tasks.json today, so live sync is effectively a no-op
archwiz/master_tasks.json in the repo is empty, so get_tasks() hits the json.JSONDecodeError path, prints a failure to stderr and returns []. Practically the sync loop never runs against real data in this checkout, which limits the blast radius of the done-detection logic but also means the code path is untested. Treating an empty file as "no tasks" (rather than an error) would avoid a spurious stderr message on every run.
Was this helpful? React with 👍 or 👎 to provide feedback.
| try: | ||
| from archwiz.sentry_init import init_sentry, capture_exception | ||
| init_sentry() | ||
| except Exception: | ||
| def capture_exception(exc): # type: ignore | ||
| pass |
There was a problem hiding this comment.
📝 Info: Fallback capture_message is not defined when only init_sentry fails in linear_client
archwiz/linear_client.py only imports and shims capture_exception, while archwiz/linear_sync.py shims both capture_exception and capture_message. That's consistent with current usage, but note the shims are only installed when the import or init_sentry() raises; if a future edit adds capture_message use in the client it would NameError in the fallback path. Splitting the import from the init_sentry() call would make the fallback semantics clearer.
Was this helpful? React with 👍 or 👎 to provide feedback.
| def init_sentry( | ||
| dsn: Optional[str] = None, | ||
| *, | ||
| project: Optional[str] = None, | ||
| traces_sample_rate: float = 1.0, | ||
| profile_session_sample_rate: float = 1.0, | ||
| profile_lifecycle: str = "trace", | ||
| enable_logs: bool = True, | ||
| send_default_pii: bool = True, | ||
| ) -> bool: |
There was a problem hiding this comment.
🟨 Personal data collection enabled by default in error reporting
init_sentry() defaults send_default_pii=True (archwiz/sentry_init.py:56, applied at archwiz/sentry_init.py:89), and both Linear helpers call it implicitly at import time (archwiz/linear_client.py:26, archwiz/linear_sync.py:28). With PII enabled, Sentry attaches request headers, cookies, request bodies and usernames/IPs to events, meaning secrets present in headers (e.g. an Authorization: lin_api_... header from the Linear calls, or user request data in the aiohttp app) can be shipped to the third-party error backend. The same default is repeated in the documented snippets (docs/SENTRY_LINEAR.md) and docs/sentry/aiohttp_example.py:8 and the Rust example.
Was this helpful? React with 👍 or 👎 to provide feedback.
| traces_sample_rate: float = 1.0, | ||
| profile_session_sample_rate: float = 1.0, | ||
| profile_lifecycle: str = "trace", | ||
| enable_logs: bool = True, |
There was a problem hiding this comment.
🟨 Full request/trace sampling enabled by default sends every event upstream
traces_sample_rate=1.0 and profile_session_sample_rate=1.0 are hardcoded defaults (archwiz/sentry_init.py:52-53), so every transaction and profile of every process is transmitted to the external Sentry service with no environment-based downscaling. Combined with send_default_pii=True, this maximizes the volume of potentially sensitive payloads leaving the device.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Implements: TER-14
Multi-platform Sentry + full Linear agent integration (protocol, CLI, AGENTS hard rules).
Sentry (4 projects under o4511844213522432)
archwiz/sentry_init.pyinit_sentry(project="aiohttp")docs/sentry/browser-init.jsdocs/sentry/rust_main_example.rsLinear — agent hooks (all agents)
docs/LINEAR-AGENT-PROTOCOL.mdarchwiz/linear_client.pystatus|start|done|comment|createarchwiz/linear_sync.pyAGENTS.mddocs/proposals/AGENTIC-PERMISSIONS.mdAgent must:
list_issues/ pick or createTER-Nlinear_client start)Implements: TER-N+ comment URL on issueValidation
Related: Manus PR #13 (path norm; Linear mock superseded) · TER-2 (tools connected) · TER-5 (dispatch logging)