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
27 changes: 23 additions & 4 deletions .claude/hooks/tapps-post-linear-list.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# tapps-mcp-hook-version: 3.12.48
# tapps-mcp-hook-content-sha: 21da2082
# tapps-mcp-hook-content-sha: 643764c9
# TappsMCP PostToolUse hook — Linear list_issues auto-populate (TAP-1412)
# After a successful mcp__plugin_linear_linear__list_issues call, write the
# response into .tapps-mcp-cache/linear-snapshots/<key>.json so the next
Expand Down Expand Up @@ -32,15 +32,24 @@ except Exception:
limit = 50
if not team or not project:
sys.exit(0)
# TAP-4588: canonicalize the open-bucket alias and drop limit from the hash so
# this writer's key matches server _resolve_cache_key / the reader.
OPEN_BUCKET = ('backlog', 'unstarted', 'started', 'triage')
def _canon_state(s):
s_lc = (s or '').strip().lower()
if s_lc == '' or s_lc == 'open' or s_lc in OPEN_BUCKET:
return 'open'
return s_lc
canon = _canon_state(state)
filt = {k: v for k, v in sorted({
'state': state, 'label': label, 'limit': limit,
'state': canon, 'label': label,
}.items()) if v not in (None, '')}
payload = json.dumps(filt, sort_keys=True, default=str).encode('utf-8')
fhash = hashlib.sha256(payload).hexdigest()[:16]
key = '__'.join([
team.replace('/', '_') or '_',
project.replace('/', '_') or '_',
(state or 'any').replace('/', '_'),
(canon.replace('/', '_') or 'any'),
fhash,
])
resp = d.get('tool_response') or d.get('toolResponse') or {}
Expand Down Expand Up @@ -69,8 +78,17 @@ def _find_issues(o):
return r
return None
issues = _find_issues(resp) or []
# TTL aligned with server-side _ttl_for_state defaults (5 min open, 1 h closed).
# TAP-4588 poisoning guard: list_issues(state='open') (a tapps-mcp alias, not a
# real Linear state) returns [] — caching that empty list under the canonical
# 'open' key would make a later get falsely report 0 issues. Skip the write
# when the raw request state was an alias/invalid AND the result is empty.
VALID_LINEAR_STATES = (
'backlog', 'unstarted', 'started', 'triage', 'completed', 'canceled'
)
state_lc = state.lower()
if not issues and state_lc and state_lc not in VALID_LINEAR_STATES:
sys.exit(0)
# TTL aligned with server-side _ttl_for_state defaults (5 min open, 1 h closed).
ttl = 3600 if state_lc in ('completed', 'canceled') else 300
now = time.time()
out = {
Expand All @@ -81,6 +99,7 @@ out = {
'team': team,
'project': project,
'auto_populated': True,
'limit': limit,
}
root = os.environ.get('TAPPS_PROJECT_ROOT') or os.getcwd()
cache_dir = os.path.join(root, '.tapps-mcp-cache', 'linear-snapshots')
Expand Down
25 changes: 17 additions & 8 deletions .claude/hooks/tapps-post-linear-snapshot-get.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# tapps-mcp-hook-version: 3.12.48
# tapps-mcp-hook-content-sha: b071ddb7
# tapps-mcp-hook-content-sha: 5a2c3acd
# TappsMCP PostToolUse hook — Linear cache-gate sentinel writer (TAP-1224)
# Writes a per-(team, project, state, label, limit) sentinel on BOTH
# cached=true and cached=false responses from tapps_linear_snapshot_get.
Expand Down Expand Up @@ -33,27 +33,36 @@ except Exception:
limit = 50
# Open-bucket alias: tapps-mcp's TTL bucket 'open' covers backlog, unstarted,
# started, triage. The skill tells agents to snapshot_get(state='open') and
# then list_issues with a concrete state. Without alias support the keys
# differ and the gate self-trips (TAP-1374). Fix: derive a bucket alias and
# emit additional sentinels for it. Same logic on both sides.
# then list_issues with a concrete state. TAP-4588: canonicalize any open
# alias ('' / 'open' / bucket member) to ONE token so the payload key and the
# sentinel key converge — matching server _canonical_state. limit is dropped
# from the hash (enforced at read time via the superset fallback). Same logic
# on both sides — see server_linear_tools._resolve_cache_key.
OPEN_BUCKET = ('backlog', 'unstarted', 'started', 'triage')
state_lc = state.lower()
def _canon_state(s):
s_lc = (s or '').strip().lower()
if s_lc == '' or s_lc == 'open' or s_lc in OPEN_BUCKET:
return 'open'
return s_lc
def _key_for(state_part: str) -> str:
canon = _canon_state(state_part)
filt = {k: v for k, v in sorted({
'state': state_part, 'label': label, 'limit': limit,
'state': canon, 'label': label,
}.items()) if v not in (None, '')}
payload = json.dumps(filt, sort_keys=True, default=str).encode('utf-8')
fhash = hashlib.sha256(payload).hexdigest()[:16]
parts = [
(team.replace('/', '_') or '_'),
(project.replace('/', '_') or '_'),
((state_part or 'any').replace('/', '_')),
(canon.replace('/', '_') or 'any'),
fhash,
]
return '__'.join(parts)
key = _key_for(state)
# Bucket alias keys: when state is 'open' (a tapps-mcp alias), '' (any), or
# any open-bucket member, every other open-bucket member should resolve.
# With canonicalization every open-bucket alias resolves to the same key, so
# the alias set is a singleton ({key}). We still emit the bucket variants and
# de-dup so the set matches the Python _alias_keys contract byte-for-byte.
alias_keys = []
if not team or not project:
key = ''
Expand Down
25 changes: 17 additions & 8 deletions .claude/hooks/tapps-pre-linear-list.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# tapps-mcp-hook-version: 3.12.48
# tapps-mcp-hook-content-sha: adb4be12
# tapps-mcp-hook-content-sha: 69a96eb2
# TappsMCP PreToolUse hook — Linear cache-first read gate (TAP-1224)
# Gates raw mcp__plugin_linear_linear__list_issues calls behind a recent
# tapps_linear_snapshot_get sentinel for the same (team, project, state,
Expand Down Expand Up @@ -36,27 +36,36 @@ except Exception:
limit = 50
# Open-bucket alias: tapps-mcp's TTL bucket 'open' covers backlog, unstarted,
# started, triage. The skill tells agents to snapshot_get(state='open') and
# then list_issues with a concrete state. Without alias support the keys
# differ and the gate self-trips (TAP-1374). Fix: derive a bucket alias and
# emit additional sentinels for it. Same logic on both sides.
# then list_issues with a concrete state. TAP-4588: canonicalize any open
# alias ('' / 'open' / bucket member) to ONE token so the payload key and the
# sentinel key converge — matching server _canonical_state. limit is dropped
# from the hash (enforced at read time via the superset fallback). Same logic
# on both sides — see server_linear_tools._resolve_cache_key.
OPEN_BUCKET = ('backlog', 'unstarted', 'started', 'triage')
state_lc = state.lower()
def _canon_state(s):
s_lc = (s or '').strip().lower()
if s_lc == '' or s_lc == 'open' or s_lc in OPEN_BUCKET:
return 'open'
return s_lc
def _key_for(state_part: str) -> str:
canon = _canon_state(state_part)
filt = {k: v for k, v in sorted({
'state': state_part, 'label': label, 'limit': limit,
'state': canon, 'label': label,
}.items()) if v not in (None, '')}
payload = json.dumps(filt, sort_keys=True, default=str).encode('utf-8')
fhash = hashlib.sha256(payload).hexdigest()[:16]
parts = [
(team.replace('/', '_') or '_'),
(project.replace('/', '_') or '_'),
((state_part or 'any').replace('/', '_')),
(canon.replace('/', '_') or 'any'),
fhash,
]
return '__'.join(parts)
key = _key_for(state)
# Bucket alias keys: when state is 'open' (a tapps-mcp alias), '' (any), or
# any open-bucket member, every other open-bucket member should resolve.
# With canonicalization every open-bucket alias resolves to the same key, so
# the alias set is a singleton ({key}). We still emit the bucket variants and
# de-dup so the set matches the Python _alias_keys contract byte-for-byte.
alias_keys = []
if not team or not project:
key = ''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2428,27 +2428,36 @@ def _memory_auto_recall_script_cursor_ps(
limit = 50
# Open-bucket alias: tapps-mcp's TTL bucket 'open' covers backlog, unstarted,
# started, triage. The skill tells agents to snapshot_get(state='open') and
# then list_issues with a concrete state. Without alias support the keys
# differ and the gate self-trips (TAP-1374). Fix: derive a bucket alias and
# emit additional sentinels for it. Same logic on both sides.
# then list_issues with a concrete state. TAP-4588: canonicalize any open
# alias ('' / 'open' / bucket member) to ONE token so the payload key and the
# sentinel key converge — matching server _canonical_state. limit is dropped
# from the hash (enforced at read time via the superset fallback). Same logic
# on both sides — see server_linear_tools._resolve_cache_key.
OPEN_BUCKET = ('backlog', 'unstarted', 'started', 'triage')
state_lc = state.lower()
def _canon_state(s):
s_lc = (s or '').strip().lower()
if s_lc == '' or s_lc == 'open' or s_lc in OPEN_BUCKET:
return 'open'
return s_lc
def _key_for(state_part: str) -> str:
canon = _canon_state(state_part)
filt = {k: v for k, v in sorted({
'state': state_part, 'label': label, 'limit': limit,
'state': canon, 'label': label,
}.items()) if v not in (None, '')}
payload = json.dumps(filt, sort_keys=True, default=str).encode('utf-8')
fhash = hashlib.sha256(payload).hexdigest()[:16]
parts = [
(team.replace('/', '_') or '_'),
(project.replace('/', '_') or '_'),
((state_part or 'any').replace('/', '_')),
(canon.replace('/', '_') or 'any'),
fhash,
]
return '__'.join(parts)
key = _key_for(state)
# Bucket alias keys: when state is 'open' (a tapps-mcp alias), '' (any), or
# any open-bucket member, every other open-bucket member should resolve.
# With canonicalization every open-bucket alias resolves to the same key, so
# the alias set is a singleton ({key}). We still emit the bucket variants and
# de-dup so the set matches the Python _alias_keys contract byte-for-byte.
alias_keys = []
if not team or not project:
key = ''
Expand Down Expand Up @@ -2658,15 +2667,24 @@ def _key_for(state_part: str) -> str:
limit = 50
if not team or not project:
sys.exit(0)
# TAP-4588: canonicalize the open-bucket alias and drop limit from the hash so
# this writer's key matches server _resolve_cache_key / the reader.
OPEN_BUCKET = ('backlog', 'unstarted', 'started', 'triage')
def _canon_state(s):
s_lc = (s or '').strip().lower()
if s_lc == '' or s_lc == 'open' or s_lc in OPEN_BUCKET:
return 'open'
return s_lc
canon = _canon_state(state)
filt = {k: v for k, v in sorted({
'state': state, 'label': label, 'limit': limit,
'state': canon, 'label': label,
}.items()) if v not in (None, '')}
payload = json.dumps(filt, sort_keys=True, default=str).encode('utf-8')
fhash = hashlib.sha256(payload).hexdigest()[:16]
key = '__'.join([
team.replace('/', '_') or '_',
project.replace('/', '_') or '_',
(state or 'any').replace('/', '_'),
(canon.replace('/', '_') or 'any'),
fhash,
])
resp = d.get('tool_response') or d.get('toolResponse') or {}
Expand Down Expand Up @@ -2695,8 +2713,17 @@ def _find_issues(o):
return r
return None
issues = _find_issues(resp) or []
# TTL aligned with server-side _ttl_for_state defaults (5 min open, 1 h closed).
# TAP-4588 poisoning guard: list_issues(state='open') (a tapps-mcp alias, not a
# real Linear state) returns [] — caching that empty list under the canonical
# 'open' key would make a later get falsely report 0 issues. Skip the write
# when the raw request state was an alias/invalid AND the result is empty.
VALID_LINEAR_STATES = (
'backlog', 'unstarted', 'started', 'triage', 'completed', 'canceled'
)
state_lc = state.lower()
if not issues and state_lc and state_lc not in VALID_LINEAR_STATES:
sys.exit(0)
# TTL aligned with server-side _ttl_for_state defaults (5 min open, 1 h closed).
ttl = 3600 if state_lc in ('completed', 'canceled') else 300
now = time.time()
out = {
Expand All @@ -2707,6 +2734,7 @@ def _find_issues(o):
'team': team,
'project': project,
'auto_populated': True,
'limit': limit,
}
root = os.environ.get('TAPPS_PROJECT_ROOT') or os.getcwd()
cache_dir = os.path.join(root, '.tapps-mcp-cache', 'linear-snapshots')
Expand Down Expand Up @@ -2787,17 +2815,25 @@ def _find_issues(o):
}
$key = ''
if ($team -and $project) {
# TAP-4588: canonicalize the open-bucket alias and drop limit from the hash
# so the PowerShell key matches server _resolve_cache_key / the reader.
$stateLc = $state.Trim().ToLower()
$openBucket = @('backlog', 'unstarted', 'started', 'triage')
if ($stateLc -eq '' -or $stateLc -eq 'open' -or $openBucket -contains $stateLc) {
$canon = 'open'
} else {
$canon = $stateLc
}
$filtObj = [ordered]@{}
if ($state) { $filtObj['state'] = $state }
if ($canon) { $filtObj['state'] = $canon }
if ($label) { $filtObj['label'] = $label }
if ($limit) { $filtObj['limit'] = $limit }
$payload = ($filtObj | ConvertTo-Json -Compress)
$sha = [System.Security.Cryptography.SHA256]::Create()
$bytes = [System.Text.Encoding]::UTF8.GetBytes($payload)
$hash = [BitConverter]::ToString($sha.ComputeHash($bytes)).Replace('-', '').ToLower().Substring(0, 16)
$teamPart = if ($team) { $team.Replace('/', '_') } else { '_' }
$projPart = if ($project) { $project.Replace('/', '_') } else { '_' }
$statePart = if ($state) { $state.Replace('/', '_') } else { 'any' }
$statePart = if ($canon) { $canon.Replace('/', '_') } else { 'any' }
$key = "${teamPart}__${projPart}__${statePart}__${hash}"
}
"""
Expand Down
Loading
Loading