Skip to content

feat: Sentry SDK + Linear GraphQL sync bridge - #16

Closed
timerloggedout-spec wants to merge 13 commits into
master-stagingfrom
feature/sentry-linear-integration
Closed

timerloggedout-spec wants to merge 13 commits into
master-stagingfrom
feature/sentry-linear-integration

Conversation

@timerloggedout-spec

@timerloggedout-spec timerloggedout-spec commented Aug 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

Implements: TER-14

Multi-platform Sentry + full Linear agent integration (protocol, CLI, AGENTS hard rules).

Sentry (4 projects under o4511844213522432)

Platform Project Entry
Python 4511844223680512 archwiz/sentry_init.py
aiohttp 4511844256055296 init_sentry(project="aiohttp")
Browser JS 4511844264640512 docs/sentry/browser-init.js
Rust 4511844272111616 docs/sentry/rust_main_example.rs

Linear — agent hooks (all agents)

Artifact Role
docs/LINEAR-AGENT-PROTOCOL.md Binding protocol: start → PR → done
archwiz/linear_client.py CLI: status|start|done|comment|create
archwiz/linear_sync.py Local taDone → Linear states
AGENTS.md Protocol in read-first + hard rules + loop
docs/proposals/AGENTIC-PERMISSIONS.md Linear marked agent-operable

Agent must:

  1. list_issues / pick or create TER-N
  2. Set In Progress (MCP or linear_client start)
  3. PR with Implements: TER-N + comment URL on issue
  4. On merge → Done + evidence

Validation

pip install "sentry-sdk"
python3 archwiz/sentry_init.py
export LINEAR_API_KEY=lin_api_...
python3 -m archwiz.linear_client status TER-14
python3 -m archwiz.linear_client start TER-14
python3 archwiz/linear_sync.py --dry-run

Related: Manus PR #13 (path norm; Linear mock superseded) · TER-2 (tools connected) · TER-5 (dispatch logging)

- 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.
@vercel

vercel Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
termux-monorepo Ready Ready Preview, v0 Aug 3, 2026 1:51am

@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e5269b4-d6de-44dc-964e-bfeb85e43327

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gitar-bot

gitar-bot Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

Gitar is working

Gitar

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 12 potential issues.

Open in Devin Review

Comment thread archwiz/linear_sync.py
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 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.

Suggested change
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
)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/linear_sync.py
Comment on lines +154 to +171
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/sentry_init.py
Comment on lines +52 to +55
logging_integration = LoggingIntegration(
level=None, # capture all levels as breadcrumbs
event_level=None, # do not auto-send log records as events
)

@devin-ai-integration devin-ai-integration Bot Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/linear_sync.py
Comment on lines +1 to +13
#!/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
"""

@devin-ai-integration devin-ai-integration Bot Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/sentry_init.py Outdated
Comment on lines +38 to +40
dsn = dsn or os.environ.get("SENTRY_DSN") or DEFAULT_DSN
if not dsn:
return False

@devin-ai-integration devin-ai-integration Bot Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/linear_sync.py
Comment on lines +39 to +51
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"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/linear_sync.py
Comment on lines +108 to +126
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/linear_sync.py
Comment on lines +218 to +223
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/linear_sync.py
Comment on lines +22 to +24
# 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 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).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/sentry_init.py

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 7 new potential issues.

Open in Devin Review

Comment thread archwiz/linear_sync.py
Comment on lines +111 to +126
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/linear_sync.py
Comment on lines +73 to +81
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 []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/linear_sync.py
Comment on lines +54 to +57
def get_tasks() -> List[Dict[str, Any]]:
master_tasks = ARCHWIZ_DIR / "master_tasks.json"
if not master_tasks.exists():
return []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/sentry_init.py
Comment on lines +48 to +57
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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/linear_sync.py
Comment on lines +26 to +33
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/sentry_init.py
Comment thread archwiz/linear_sync.py

Copy link
Copy Markdown
Owner Author

Linear agent protocol landed (Implements: TER-14)

  • docs/LINEAR-AGENT-PROTOCOL.md — binding hooks for all agents
  • archwiz/linear_client.py — status|start|done|comment|create
  • AGENTS.md — Linear in read-first + hard rules + execution loop

Agents must:

  1. Start → Linear In Progress
  2. PR → Implements: TER-N + comment URL on issue
  3. Merge → Done + evidence

MCP: linear___save_issue / linear___list_issues
CLI: python3 -m archwiz.linear_client start TER-14

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 5 new potential issues.

Open in Devin Review

Comment thread archwiz/linear_client.py
Comment on lines +108 to +125
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/linear_sync.py
Comment on lines +202 to +204
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}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/linear_sync.py
Comment on lines +26 to +33
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/linear_client.py
Comment on lines +32 to +33
TEAM_NAME = os.environ.get("LINEAR_TEAM", "Termux-monorepo_linear")
PROJECT_NAME = os.environ.get("LINEAR_PROJECT", "termux-monorepo hardening")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/linear_client.py

Copy link
Copy Markdown
Owner Author

Ack TER-14 comment (Operator):

master-staging is for selective merge to master meaning master-staging is meant to never merge to master completely.

Reflected in repo (this PR):

  • AGENTS.md — hard rule: permanent spine; never wholesale staging→master
  • docs/LINEAR-AGENT-PROTOCOL.md — §0 branch model + Done = merge to master-staging only
  • docs/proposals/AGENTIC-PERMISSIONS.md — branch model diagram; removed “promote staging wholesale” language

ARCHW1Z-GATE.md on master-staging already treats staging as the integration path and master as the final ratchet — consistent. No change that deletes or flattens master-staging as a gate.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 6 new potential issues.

Open in Devin Review

Comment thread archwiz/sentry_init.py
Comment on lines +63 to +69
global _initialized
if _initialized:
return True

resolved = _resolve_dsn(dsn, project)
if not resolved:
return False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/sentry_init.py
Comment on lines +87 to +97
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"),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/linear_sync.py
Comment on lines +54 to +70
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)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/linear_client.py
Comment on lines +24 to +29
try:
from archwiz.sentry_init import init_sentry, capture_exception
init_sentry()
except Exception:
def capture_exception(exc): # type: ignore
pass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📝 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/sentry_init.py
Comment on lines +48 to +57
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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟨 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread archwiz/sentry_init.py
Comment on lines +52 to +55
traces_sample_rate: float = 1.0,
profile_session_sample_rate: float = 1.0,
profile_lifecycle: str = "trace",
enable_logs: bool = True,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟨 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

This branch was successfully deployed

1 active deployment
Preview — 400271fd Deployed Aug 3, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant