#!/usr/bin/env bash
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)"
cd "$ROOT"

failures=0
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT

fail() {
  printf 'FAIL: %s\n' "$1" >&2
  failures=$((failures + 1))
}

require_contains() {
  local file="$1"
  local pattern="$2"
  local description="$3"
  if [[ ! -f "$file" ]]; then
    fail "$description ($file is missing)"
    return
  fi
  if ! grep -Eq "$pattern" "$file"; then
    fail "$description ($file must contain pattern: $pattern)"
  fi
}

require_contains_multiline() {
  local file="$1"
  local pattern="$2"
  local description="$3"
  if [[ ! -f "$file" ]]; then
    fail "$description ($file is missing)"
    return
  fi
  if ! python3 - "$file" "$pattern" <<'PY'
import re
import sys
from pathlib import Path

source = Path(sys.argv[1]).read_text(encoding="utf-8")
pattern = sys.argv[2]
sys.exit(0 if re.search(pattern, source, re.S) else 1)
PY
  then
    fail "$description ($file must contain pattern: $pattern)"
  fi
}

require_not_contains_multiline() {
  local file="$1"
  local pattern="$2"
  local description="$3"
  if [[ ! -f "$file" ]]; then
    fail "$description ($file is missing)"
    return
  fi
  if python3 - "$file" "$pattern" <<'PY'
import re
import sys
from pathlib import Path

source = Path(sys.argv[1]).read_text(encoding="utf-8")
pattern = sys.argv[2]
sys.exit(0 if re.search(pattern, source, re.S) else 1)
PY
  then
    fail "$description ($file must not contain multiline pattern: $pattern)"
  fi
}

require_file_absent() {
  local file="$1"
  local description="$2"
  if [[ -e "$file" ]]; then
    fail "$description ($file must not exist)"
  fi
}

require_go_struct_contains() {
  local file="$1"
  local struct_name="$2"
  local pattern="$3"
  local description="$4"
  if [[ ! -f "$file" ]]; then
    fail "$description ($file is missing)"
    return
  fi
  if ! python3 - "$file" "$struct_name" "$pattern" <<'PY'
import re
import sys
from pathlib import Path

path = Path(sys.argv[1])
struct_name = sys.argv[2]
pattern = sys.argv[3]
source = path.read_text(encoding="utf-8")
match = re.search(r"type\s+" + re.escape(struct_name) + r"\s+struct\s*\{(?P<body>.*?)\n\}", source, re.S)
if not match:
    sys.exit(1)
body = match.group("body")
sys.exit(0 if re.search(pattern, body, re.M) else 1)
PY
  then
    fail "$description ($file $struct_name must contain pattern: $pattern)"
  fi
}

require_ts_type_contains() {
  local file="$1"
  local type_name="$2"
  local pattern="$3"
  local description="$4"
  if [[ ! -f "$file" ]]; then
    fail "$description ($file is missing)"
    return
  fi
  if ! python3 - "$file" "$type_name" "$pattern" <<'PY'
import re
import sys
from pathlib import Path

path = Path(sys.argv[1])
type_name = sys.argv[2]
pattern = sys.argv[3]
source = Path(path).read_text(encoding="utf-8")
match = re.search(r"export\s+type\s+" + re.escape(type_name) + r"\s*=\s*\{(?P<body>.*?)\n\}", source, re.S)
if not match:
    sys.exit(1)
body = match.group("body")
sys.exit(0 if re.search(pattern, body, re.M) else 1)
PY
  then
    fail "$description ($file $type_name must contain pattern: $pattern)"
  fi
}

require_im_dependency_field() {
  local file="$1"
  local field="$2"
  local description="$3"
  if [[ ! -f "$file" ]]; then
    fail "$description ($file is missing)"
    return
  fi
  if ! python3 - "$file" "$field" <<'PY'
from pathlib import Path
import sys

path = Path(sys.argv[1])
field = sys.argv[2]
source = path.read_text(encoding="utf-8")
marker = "IM: im.Dependencies{"
start = source.find(marker)
if start < 0:
    sys.exit(1)
end = source.find("\n\t\t\t}", start)
if end < 0:
    sys.exit(1)
block = source[start:end]
sys.exit(0 if field in block else 1)
PY
  then
    fail "$description ($file im.Dependencies must contain: $field)"
  fi
}

path_matches_any_glob_file() {
  local file="$1"
  local globs_file="$2"
  local glob

  [[ -s "$globs_file" ]] || return 1
  while IFS= read -r glob; do
    [[ -z "$glob" ]] && continue
    [[ "$file" == $glob || "$(basename "$file")" == $glob ]] && return 0
  done < "$globs_file"
  return 1
}

grep_matches() {
  local pattern="$1"
  shift

  local roots_file files_file includes_file excludes_file selected_file
  roots_file="$(mktemp "$tmp_dir/roots.XXXXXX")"
  files_file="$(mktemp "$tmp_dir/files.XXXXXX")"
  includes_file="$(mktemp "$tmp_dir/includes.XXXXXX")"
  excludes_file="$(mktemp "$tmp_dir/excludes.XXXXXX")"
  selected_file="$(mktemp "$tmp_dir/selected.XXXXXX")"

  while (( $# > 0 )); do
    case "$1" in
      --glob)
        if [[ "${2:-}" == '!'* ]]; then
          printf '%s\n' "${2:1}" >> "$excludes_file"
        else
          printf '%s\n' "$2" >> "$includes_file"
        fi
        shift 2
        ;;
      *)
        printf '%s\n' "$1" >> "$roots_file"
        shift
        ;;
    esac
  done

  local root
  while IFS= read -r root; do
    [[ -z "$root" ]] && continue
    if [[ -f "$root" ]]; then
      printf '%s\n' "$root"
    elif [[ -d "$root" ]]; then
      git ls-files -- "$root"
    fi
  done < "$roots_file" > "$files_file"

  local file
  while IFS= read -r file; do
    [[ -z "$file" ]] && continue
    if [[ -s "$includes_file" ]] && ! path_matches_any_glob_file "$file" "$includes_file"; then
      continue
    fi
    if path_matches_any_glob_file "$file" "$excludes_file"; then
      continue
    fi
    printf '%s\n' "$file" >> "$selected_file"
  done < "$files_file"

  [[ -s "$selected_file" ]] || return 0
  while IFS= read -r file; do
    grep -HEn -- "$pattern" "$file" 2>/dev/null || true
  done < "$selected_file"
}

require_no_matches() {
  local description="$1"
  shift
  local matches
  matches=$(grep_matches "$@")
  if [[ -n "$matches" ]]; then
    fail "$description"
    printf '%s\n' "$matches" >&2
  fi
}

require_no_matches_except_paths() {
  local description="$1"
  local pattern="$2"
  local allowed_path_regex="$3"
  shift 3

  local matches
  matches=$(grep_matches "$pattern" "$@" | grep -Ev "^(${allowed_path_regex}):" || true)
  if [[ -n "$matches" ]]; then
    fail "$description"
    printf '%s\n' "$matches" >&2
  fi
}

require_no_multiline_matches() {
  local description="$1"
  local pattern="$2"
  shift 2

  local matches=""
  local file
  for file in "$@"; do
    [[ -f "$file" ]] || continue
    local file_matches
    file_matches=$(python3 - "$file" "$pattern" <<'PY'
import re
import sys
from pathlib import Path

path = Path(sys.argv[1])
pattern = sys.argv[2]
source = path.read_text(encoding="utf-8")
for match in re.finditer(pattern, source, re.S):
    line = source.count("\n", 0, match.start()) + 1
    snippet = " ".join(match.group(0).split())
    print(f"{path}:{line}:{snippet}")
PY
)
    if [[ -n "$file_matches" ]]; then
      matches+="$file_matches"$'\n'
    fi
  done
  if [[ -n "$matches" ]]; then
    fail "$description"
    printf '%s' "$matches" >&2
  fi
}

require_contains internal/store/tasktimeline/writer.go 'func WritePlan\(' 'task timeline writer must expose the unified WritePlan entrypoint'
require_contains internal/store/tasktimeline/writer.go 'func WritePlanWithResult\(' 'task timeline writer must expose result-bearing writes for downstream source/provenance plans'
require_go_struct_contains internal/store/tasktimeline/writer.go TaskTimelinePlan 'TypedActivityIntents\s+\[\]TypedActivityIntent' 'task timeline plan must accept typed event intents, not feature-built projections'
require_contains internal/store/tasktimeline/writer.go 'func NewTypedActivityIntent\(' 'task timeline writer must expose typed event intent construction'
require_contains internal/store/tasktimeline/writer.go 'typedActivityProjectionsFromIntents\(plan\.TypedActivityIntents\)' 'task timeline writer must allocate task event projections from canonical intents'
require_contains internal/store/tasktimeline/writer.go 'INSERT INTO scope_activity_events' 'task timeline writer must own typed activity projection inserts'
require_no_multiline_matches \
  'task timeline writes must not reintroduce runtime-owned task unread counter increments' \
  'IntentKindMessageUnread|NewMessageUnreadIntent|task_message_unread_count\s*=\s*conversation_member_states\.task_message_unread_count|task_activity_unread_count\s*=\s*conversation_member_states\.task_activity_unread_count' \
  internal/store/tasktimeline/writer.go
require_no_matches \
  'legacy task attention/unread cleanup code must stay removed after schema cleanup' \
  'ClearLegacyUnreadCounters|task_message_unread_count|task_activity_unread_count|task_inbox_active|task_inbox_sort_at|taskattention\.' \
  internal/store internal/api internal/task cmd e2e \
  --glob '*.go' \
  --glob '!*_test.go'
require_no_matches \
  'retired tasksearch and tasknotifications package paths must stay removed' \
  'github\.com/tidbcloud/AgentTeam/internal/(store/tasksearch|task/tasknotifications)|tasksearch\.|tasknotifications\.' \
  internal \
  --glob '*.go'
require_contains internal/store/taskboard/task_signals.go 'func highSignalActivityPredicateSQL\(' 'taskboard must own the bounded high-signal summary predicate'
require_contains internal/store/taskboard/search.go 'func browseSearchTextSQL\(' 'taskboard must own canonical task profile browse/search SQL'
require_contains internal/store/taskboard/search.go 'func browseSearchLikePattern\(' 'taskboard must own task profile browse/search query normalization'
require_contains internal/store/taskboard/search.go 'type taskSearchPlan struct' 'taskboard search must expose one canonical search plan'
require_contains internal/store/taskboard/search.go 'func searchTaskProfileIDs\(' 'taskboard search must resolve profile queries to task ids before browse filtering'
require_contains internal/store/taskboard/browse_query.go 'searchTaskProfileIDs\(ctx, txs\.db' 'taskboard browse must consume resolved search task ids instead of inlining profile SQL'
require_contains internal/store/taskboard/query_builders.go 'func buildTaskBrowseSearchIDCondition\(' 'taskboard browse must filter by resolved search task ids'
require_contains internal/store/taskboard/browse_contract.go 'normalizeBrowseSearchQuery\(input\.SearchQuery\)' 'task browse normalization must use the taskboard-owned search helper'
require_contains internal/store/taskboard/detail.go 'TypedActivityIntents:[[:space:]]+intents' 'taskboard mutation adapter must pass event intents to the task timeline plan'
require_file_absent internal/task/taskdispatch/plan.go 'retired task dispatch write-plan package must stay removed'
require_file_absent internal/store/app/task_dispatch_outbox.go 'retired task dispatch outbox writer must stay removed'
require_file_absent internal/api/task/dispatch.go 'retired human Task dispatch handler must stay removed'
require_file_absent internal/api/task/task_rollback.go 'retired Task create-mode rollback helper must stay removed'
require_no_matches \
  'Task mutation production code must not recreate dispatch_requested facts or hidden Task dispatch writers' \
  'dispatch_requested|taskEventDispatchRequested|SendTaskDispatchFor(User|Agent)WithMetadata|InsertTaskDispatchOutbox|taskdispatch\.' \
  internal/store/taskboard internal/api/task internal/api/agent internal/task \
  --glob '*.go' \
  --glob '!*_test.go'
require_contains internal/store/message/message_semantics.go 'MessageMigrationTaskDispatchDeliveryIntent' 'legacy task_dispatch packets must stay registered until mixed-version queues are drained'
require_contains internal/store/message/message_semantics.go 'MessageTypeTaskDispatch:[[:space:]]+\{' 'legacy task_dispatch packets must retain receive semantics during the rolling window'
require_contains_multiline internal/store/message/message_semantics.go 'MessageTypeTaskDispatch:\s*\{.*?CurrentWriteDisposition:\s*MessageWriteLegacyReadOnly.*?TargetWriteDisposition:\s*MessageWriteDisallowed' 'legacy task_dispatch must be readable/replayable but closed to every active writer'
require_contains_multiline internal/api/im/fanout/scope_fanout_replay.go 'case message\.DeliverySourceRefMessage:\s*return adapter\.fanoutMessagePayload' 'scope fanout replay must continue accepting old-node message outbox payloads'
require_contains internal/api/im/fanout/scope_fanout_replay_test.go 'TestScopeFanoutReplayAdapterReplaysLegacyTaskDispatchPacketToAgentOnly' 'rolling regression must prove old-node task_dispatch packets still reach agents without human realtime'
require_contains internal/api/im/fanout/scope_fanout_replay_test.go 'TestScopeFanoutReplayAdapterReplaysLegacyTaskDispatchActivityIntentToAgentOnly' 'rolling regression must prove old-node activity-event task_dispatch intents still reach agents'
require_contains internal/store/app/app_messages_integration_test.go 'TestSendTaskDispatchMessageTypeIsLegacyReadOnlyForActiveWrites' 'generic message append must reject new task_dispatch carriers'
require_contains internal/api/daemon/protocol_autowake_integration_test.go 'TestHubTaskCommandNotifyBypassesNoWakePreference' 'old-daemon task command compatibility must retain its receive/wake coverage'
require_no_matches_except_paths \
  'production code must not reopen or write task_dispatch outside the retained store/message read compatibility descriptor' \
  'MessageTypeTaskDispatch|"task_dispatch"' \
  'internal/store/message/(message_types|message_semantics)\.go' \
  internal cmd e2e \
  --glob '*.go' \
  --glob '!*_test.go'
require_contains internal/store/app/service_event_task_delivery.go 'TypedActivityIntents:[[:space:]]+\[\]tasktimeline\.TypedActivityIntent' 'service-event task delivery must declare typed event intents, not feature-built projections'
require_contains specs/task/task-timeline-messageization.md 'TypedActivityIntents' 'task timeline messageization spec must define the typed event intent plan contract'
require_contains specs/task/task-timeline-messageization.md 'fallback' 'task timeline messageization spec must forbid fallback or compatibility write paths'
require_contains internal/api/dto/task_browser.go 'CreatedByProfile[[:space:]]+\*AuthorProfile' 'task artifact DTO must expose canonical creator profile'
require_contains internal/api/dto/task_browser.go 'RequestedByProfile[[:space:]]+\*AuthorProfile' 'task review DTO must expose canonical requester profile'
require_contains internal/api/dto/task_browser.go 'ReviewerProfile[[:space:]]+\*AuthorProfile' 'task review DTO must expose canonical reviewer profile'
require_contains internal/api/dto/task_browser.go 'DecidedByProfile[[:space:]]+\*AuthorProfile' 'task review DTO must expose canonical decider profile'
require_go_struct_contains internal/api/task/task_contract.go TaskArtifact 'CreatedByProfile\s+\*AuthorProfile\s+`json:"createdByProfile,omitempty"`' 'internal task artifact contract must expose canonical creator profile'
require_go_struct_contains internal/api/task/task_contract.go TaskReviewRequest 'RequestedByProfile\s+\*AuthorProfile\s+`json:"requestedByProfile,omitempty"`' 'internal task review contract must expose canonical requester profile'
require_go_struct_contains internal/api/task/task_contract.go TaskReviewRequest 'ReviewerProfile\s+\*AuthorProfile\s+`json:"reviewerProfile,omitempty"`' 'internal task review contract must expose canonical reviewer profile'
require_go_struct_contains internal/api/task/task_contract.go TaskReviewRequest 'DecidedByProfile\s+\*AuthorProfile\s+`json:"decidedByProfile,omitempty"`' 'internal task review contract must expose canonical decider profile'
require_go_struct_contains internal/api/agent/task_contract.go TaskArtifact 'CreatedByProfile\s+\*AuthorProfile\s+`json:"createdByProfile,omitempty"`' 'agent task artifact contract must expose canonical creator profile'
require_contains internal/store/taskboard/detail_artifacts_checkpoints.go 'hydrateTaskArtifactPrincipalProfiles' 'task artifact read path must hydrate canonical creator profiles'
require_contains internal/store/taskboard/detail_review.go 'hydrateTaskReviewPrincipalProfiles' 'task review read path must hydrate canonical principal profiles'
require_contains internal/store/taskboard/detail_review.go 'return hydratedTaskReviewResponse\(ctx, tx, task, review\)' 'task review no-op mutation response must hydrate canonical profiles before returning'
require_contains internal/store/taskboard/review_integration_test.go 'noopReassignReview\.Review\.ReviewerProfile' 'task review no-op reassign response must have a canonical reviewer profile regression test'
require_contains internal/api/dto/converter/task.go 'taskFactCompatRefFromAuthorProfile' 'task fact compat refs must be derived from canonical profiles'
require_contains internal/api/task/taskboard_adapter.go 'CreatedByProfile:[[:space:]]+createdByProfile' 'internal task artifact adapter must preserve canonical creator profile'
require_contains internal/api/task/taskboard_adapter.go 'CreatedByRef:[[:space:]]+taskFactCompatRefFromAuthorProfile\(createdByProfile, artifact\.CreatedByRef\)' 'internal task artifact adapter must derive legacy creator ref from canonical profile'
require_contains internal/api/task/taskboard_adapter.go 'RequestedByProfile:[[:space:]]+requestedByProfile' 'internal task review adapter must preserve canonical requester profile'
require_contains internal/api/task/taskboard_adapter.go 'ReviewerProfile:[[:space:]]+reviewerProfile' 'internal task review adapter must preserve canonical reviewer profile'
require_contains internal/api/task/taskboard_adapter.go 'DecidedByProfile:[[:space:]]+decidedByProfile' 'internal task review adapter must preserve canonical decider profile'
require_contains internal/api/task/taskboard_adapter.go 'RequestedByRef:[[:space:]]+taskFactCompatRefFromAuthorProfile\(requestedByProfile, review\.RequestedByRef\)' 'internal task review adapter must derive legacy requester ref from canonical profile'
require_contains internal/api/task/taskboard_adapter.go 'ReviewerRef:[[:space:]]+taskFactCompatRefFromAuthorProfile\(reviewerProfile, review\.ReviewerRef\)' 'internal task review adapter must derive legacy reviewer ref from canonical profile'
require_contains internal/api/task/taskboard_adapter.go 'DecidedByRef:[[:space:]]+taskFactCompatRefFromAuthorProfile\(decidedByProfile, review\.DecidedByRef\)' 'internal task review adapter must derive legacy decider ref from canonical profile'
require_contains internal/api/agent/taskboard_adapter.go 'CreatedByProfile:[[:space:]]+createdByProfile' 'agent task artifact adapter must preserve canonical creator profile'
require_contains internal/api/agent/taskboard_adapter.go 'CreatedByRef:[[:space:]]+taskFactCompatRefFromAuthorProfile\(createdByProfile, artifact\.CreatedByRef\)' 'agent task artifact adapter must derive legacy creator ref from canonical profile'
require_contains internal/api/task/dto_conversion.go 'CreatedByProfile:[[:space:]]+taskboardTaskPrincipalProfileFromProfile\(createdByProfile\)' 'internal task reverse bridge must preserve canonical artifact creator profile'
require_contains internal/api/task/dto_conversion.go 'CreatedByRef:[[:space:]]+taskboardTaskFactCompatRefFromAuthorProfile\(createdByProfile, artifact\.CreatedByRef\)' 'internal task reverse bridge must derive artifact compat ref from canonical profile'
require_contains internal/api/task/dto_conversion.go 'RequestedByProfile:[[:space:]]+taskboardTaskPrincipalProfileFromProfile\(requestedByProfile\)' 'internal task reverse bridge must preserve canonical requester profile'
require_contains internal/api/task/dto_conversion.go 'ReviewerProfile:[[:space:]]+taskboardTaskPrincipalProfileFromProfile\(reviewerProfile\)' 'internal task reverse bridge must preserve canonical reviewer profile'
require_contains internal/api/task/dto_conversion.go 'DecidedByProfile:[[:space:]]+taskboardTaskPrincipalProfileFromProfile\(decidedByProfile\)' 'internal task reverse bridge must preserve canonical decider profile'
require_contains internal/api/task/dto_conversion.go 'RequestedByRef:[[:space:]]+taskboardTaskFactCompatRefFromAuthorProfile\(requestedByProfile, review\.RequestedByRef\)' 'internal task reverse bridge must derive requester compat ref from canonical profile'
require_contains internal/api/task/dto_conversion.go 'ReviewerRef:[[:space:]]+taskboardTaskFactCompatRefFromAuthorProfile\(reviewerProfile, review\.ReviewerRef\)' 'internal task reverse bridge must derive reviewer compat ref from canonical profile'
require_contains internal/api/task/dto_conversion.go 'DecidedByRef:[[:space:]]+taskboardTaskFactCompatRefFromAuthorProfile\(decidedByProfile, review\.DecidedByRef\)' 'internal task reverse bridge must derive decider compat ref from canonical profile'
require_contains internal/api/agent/dto_conversion.go 'CreatedByProfile:[[:space:]]+taskboardTaskPrincipalProfileFromProfile\(createdByProfile\)' 'agent task reverse bridge must preserve canonical artifact creator profile'
require_contains internal/api/agent/dto_conversion.go 'CreatedByRef:[[:space:]]+taskboardTaskFactCompatRefFromAuthorProfile\(createdByProfile, artifact\.CreatedByRef\)' 'agent task reverse bridge must derive artifact compat ref from canonical profile'
require_no_matches \
  'long-lived specs and frontend cache inventory must not describe task-native discussion edit/delete/reaction as the target path' \
  'task-native discussion reactions|task discussion reaction / edit / delete.*task-native|task-native edit/delete/reaction|discussion entries .*should not patch message reaction aggregates' \
  specs/task \
  --glob '*.md' \
  --glob '*.ts'


require_no_matches \
	'production task typed activity writers must declare TypedActivityIntent entries and must not build projection rows or use retired helper names outside the writer' \
	'NewTypedActivityProjection|TypedActivities:|writeTaskTimelineActivity|syncTaskInboxProjectionForTask|incrementTaskActivityUnreadProjection|syncTaskInboxProjection\(' \
	internal/store/taskboard internal/store/app internal/api internal/task cmd e2e \
	--glob '*.go' \
	--glob '!*_test.go'

require_no_matches \
  'Task APIs must not retain dispatch/create-mode dependencies after the assignee cutover' \
  'TaskDispatch(Store|Notifier)|DispatchStore|CreateMode|createMode|create_mode' \
  internal/api/task internal/api/agent internal/store/taskboard \
  --glob '*.go' \
  --glob '!*_test.go'

require_no_matches \
  'task attention/unread/inbox projection writes must not live in message, tasktimeline, or taskboard packages' \
  'ApplyTaskMessageUnreadProjectionTx|TaskUnreadProjection:|func (incrementTaskActivityUnreadProjection|syncTaskInboxProjection|updateTaskHighSignalProjection)\b|task_message_unread_count = conversation_member_states\.task_message_unread_count|task_activity_unread_count = conversation_member_states\.task_activity_unread_count' \
  internal/store/message internal/store/tasktimeline internal/store/taskboard internal/store/app internal/api internal/task cmd e2e \
  --glob '*.go' \
  --glob '!*_test.go'

require_no_matches \
  'taskboard query builders must not keep private copies of task profile search SQL; use taskboard search.go' \
  'current_agent_member\.display_name|reviewer_user\.display_name|claimed_user\.display_name|BrowseSearchTextSQL|BrowseSearchLikePattern|BuildBrowseSearchCondition|CONCAT_WS\(' \
  internal/store/taskboard/query_builders.go \
  --glob '*.go'

require_no_matches \
  'taskboard browse runtime must resolve profile search through its search owner before consuming task ids' \
  'BrowseSearchTextSQL|BrowseSearchLikePattern|BuildBrowseSearchCondition|LIKE[[:space:]]+\$' \
  internal/store/taskboard/browse_query.go internal/store/taskboard/query_builders.go \
  --glob '*.go'

require_no_matches \
  'task board Web workbench must not re-implement profile search locally; render backend-browsed task pages' \
  'normalizeTaskBoardSearchQuery|buildTaskBoardSearchText|taskMatchesSearchQuery' \
  web/src/agentteam/workspace/tasks/board \
  --glob '*.ts' \
  --glob '*.tsx' \
  --glob '!*test.ts' \
  --glob '!*test.tsx'

require_no_matches \
  'task browse contract must not add a second search normalization path' \
  'normalizeTaskBoardSearchQuery\(input\.SearchQuery\)' \
  internal/store/taskboard/browse_contract.go \
  --glob '*.go'

# The task_event dispatcher may transport writer-produced projections from
# tasktimeline.WritePlanWithResult to the Task-event sink after commit, but it
# must not construct projection rows itself. Keep all other production code on
# TypedActivityIntent.
require_no_matches_except_paths \
  'production task feature code must not allocate task activity projection structs directly; projections are writer-internal output of TypedActivityIntent' \
  'TypedActivityProjection' \
  'internal/store/taskboard/(store_events|task_event_collector|task_event_dispatch)\.go' \
  internal/store/taskboard internal/store/app internal/api internal/task cmd e2e \
  --glob '*.go' \
  --glob '!*_test.go'

require_contains internal/api/dto/task_browser.go 'Timeline[[:space:]]+\[\]TaskTimelineEntry' 'browser task detail DTO must expose one unified timeline array'
require_contains internal/api/dto/task_browser.go 'AuthorProfile[[:space:]]+\*AuthorProfile[[:space:]]+`json:"authorProfile"`' 'task discussion DTO must expose canonical message AuthorProfile'
require_contains internal/api/dto/task_browser.go 'Source[[:space:]]+\*ContributionSource[[:space:]]+`json:"source,omitempty"`' 'task discussion DTO must expose canonical message ContributionSource'
require_contains internal/api/dto/task_browser.go 'CreatedBy is retained as a compatibility adapter' 'task discussion CreatedBy must be documented as compatibility-derived, not canonical'
require_contains internal/api/task/task_contract.go 'AuthorProfile[[:space:]]+\*AuthorProfile[[:space:]]+`json:"authorProfile"`' 'internal task discussion contract must carry canonical message AuthorProfile through adapters'
require_contains internal/api/task/task_contract.go 'Source[[:space:]]+\*ContributionSource[[:space:]]+`json:"source,omitempty"`' 'internal task discussion contract must carry canonical message ContributionSource through adapters'
require_contains internal/api/task/taskboard_adapter.go 'authorProfile[[:space:]]+:=[[:space:]]+authorProfileFromTaskboard\(entry\.AuthorProfile\)' 'task API adapter must preserve task discussion AuthorProfile'
require_contains internal/api/task/taskboard_adapter.go 'Source:[[:space:]]+contributionSourceFromTaskboard\(entry\.Source\)' 'task API adapter must preserve task discussion ContributionSource'
require_contains internal/api/task/taskboard_adapter.go 'CreatedBy:[[:space:]]+taskDiscussionCompatActorFromAuthorProfile\(authorProfile, entry\.CreatedBy\)' 'task API adapter must derive legacy CreatedBy from canonical AuthorProfile'
require_contains internal/api/task/dto_conversion.go 'authorProfile[[:space:]]+:=[[:space:]]+taskboardAuthorProfileFromDTO\(entry\.AuthorProfile\)' 'task DTO bridge must preserve task discussion AuthorProfile'
require_contains internal/api/task/dto_conversion.go 'Source:[[:space:]]+taskboardContributionSourceFromDTO\(entry\.Source\)' 'task DTO bridge must preserve task discussion ContributionSource'
require_contains internal/api/task/dto_conversion.go 'CreatedBy:[[:space:]]+taskboardCompatActorFromAuthorProfile\(authorProfile, entry\.CreatedBy\)' 'task DTO bridge must derive legacy CreatedBy from canonical AuthorProfile'
require_contains internal/api/agent/task_contract.go 'AuthorProfile[[:space:]]+\*AuthorProfile[[:space:]]+`json:"authorProfile"`' 'agent task discussion contract must carry canonical message AuthorProfile through MCP/task adapters'
require_contains internal/api/agent/task_contract.go 'Source[[:space:]]+\*ContributionSource[[:space:]]+`json:"source,omitempty"`' 'agent task discussion contract must carry canonical message ContributionSource through MCP/task adapters'
require_contains internal/api/agent/task_contract.go 'CreatedBy is retained only as an agent task API compatibility adapter' 'agent task discussion CreatedBy must be documented as compatibility-derived, not canonical'
require_contains internal/api/agent/taskboard_adapter.go 'authorProfile[[:space:]]+:=[[:space:]]+authorProfileFromTaskboard\(entry\.AuthorProfile\)' 'agent task API adapter must preserve task discussion AuthorProfile'
require_contains internal/api/agent/taskboard_adapter.go 'Source:[[:space:]]+contributionSourceFromTaskboard\(entry\.Source\)' 'agent task API adapter must preserve task discussion ContributionSource'
require_contains internal/api/agent/taskboard_adapter.go 'CreatedBy:[[:space:]]+taskDiscussionCompatActorFromAuthorProfile\(authorProfile, entry\.CreatedBy\)' 'agent task API adapter must derive legacy CreatedBy from canonical AuthorProfile'
require_contains internal/api/agent/dto_conversion.go 'authorProfile[[:space:]]+:=[[:space:]]+taskboardAuthorProfileFromDTO\(entry\.AuthorProfile\)' 'agent task DTO bridge must preserve task discussion AuthorProfile'
require_contains internal/api/agent/dto_conversion.go 'Source:[[:space:]]+taskboardContributionSourceFromDTO\(entry\.Source\)' 'agent task DTO bridge must preserve task discussion ContributionSource'
require_contains internal/api/agent/dto_conversion.go 'CreatedBy:[[:space:]]+taskboardCompatActorFromAuthorProfile\(authorProfile, entry\.CreatedBy\)' 'agent task DTO bridge must derive legacy CreatedBy from canonical AuthorProfile'
require_contains internal/api/dto/converter/task.go 'authorProfile[[:space:]]+:=[[:space:]]+ToAuthorProfile\(entry\.AuthorProfile\)' 'task discussion DTO converter must map canonical AuthorProfile from the message-backed read model'
require_contains internal/api/dto/converter/task.go 'Source:[[:space:]]+ToContributionSource\(entry\.Source\)' 'task discussion DTO converter must map canonical ContributionSource from the message-backed read model'
require_contains internal/api/dto/converter/task.go 'CreatedBy:[[:space:]]+taskDiscussionCompatActorFromAuthorProfile\(authorProfile, entry\.CreatedBy\)' 'task discussion DTO converter must derive legacy CreatedBy from canonical AuthorProfile'
require_contains internal/api/dto/converter/task.go 'func taskDiscussionCompatActorFromAuthorProfile' 'task discussion DTO converter must keep legacy CreatedBy as an explicit boundary adapter'
require_contains internal/store/taskboard/types.go 'AuthorProfile[[:space:]]+message\.AuthorProfile' 'task discussion store read model must carry message AuthorProfile'
require_contains internal/store/taskboard/types.go 'Source[[:space:]]+message\.ContributionSource' 'task discussion store read model must carry message ContributionSource'
require_contains internal/store/taskboard/actor_projection.go 'type taskDiscussionMessageAuthorProjection' 'task discussion primary author projection must be message-author specific, not TaskActor-only'
require_contains internal/store/taskboard/discussion_page.go 'taskDiscussionMessageAuthorProjection' 'task discussion scan must hydrate a message AuthorProfile projection before deriving compatibility CreatedBy'
require_contains internal/store/taskboard/discussion_page.go 'AttachContributionSourcesForAppMessages' 'task discussion read path must use the message contribution source resolver'
require_contains internal/store/taskboard/discussion_page.go 'JOIN scope_messages sm' 'task discussion read path must use scope_messages as the runtime source of truth'
require_contains internal/store/taskboard/discussion_page.go 'sm\.author_member_id' 'task discussion read path must derive authors from scope_messages.author_member_id'
require_contains internal/store/taskboard/discussion_page.go 'service_members created_service_member' 'task discussion read path must hydrate service authors from service_members'
require_contains internal/store/taskboard/discussion_page.go 'service_definitions created_service_definition' 'task discussion read path must hydrate service author display from service_definitions'
require_ts_type_contains web/src/types/tasks.ts TaskDiscussionEntry 'authorProfile:\s+AuthorProfile' 'frontend task discussion type must expose canonical message AuthorProfile'
require_ts_type_contains web/src/types/tasks.ts TaskDiscussionEntry 'source\?:\s+MessageContributionSource' 'frontend task discussion type must expose canonical message ContributionSource'
require_contains specs/task/task-timeline-messageization.md 'authorProfile \+ source' 'task timeline spec must document canonical task discussion author/source DTO fields'
require_contains specs/task/task-timeline-messageization.md 'Strict Cutover Audit Evidence Matrix' 'task timeline spec must include a strict cutover evidence matrix'
require_contains specs/task/task-timeline-messageization.md 'Typed task activity write/read/attention plan' 'task timeline spec must document typed activity write/read/attention strict cutover'
require_contains internal/store/message/contribution_source.go 'ContributionSourceRefTypeTaskActivity[[:space:]]+= "task_activity"' 'message contribution source substrate must name task activity provenance refs'
require_contains internal/store/taskboard/types.go 'Canonical activity identity and provenance are AuthorProfile and Source' 'task activity store read model must document legacy actor/source fields as compatibility aliases'
require_go_struct_contains internal/store/taskboard/types.go TaskActivityEvent 'AuthorProfile\s+message\.AuthorProfile' 'task activity store read model must carry canonical AuthorProfile'
require_go_struct_contains internal/store/taskboard/types.go TaskActivityEvent 'Source\s+message\.ContributionSource' 'task activity store read model must carry canonical ContributionSource'
require_contains internal/api/dto/task_browser.go 'Task activity identity and' 'browser task activity DTO must document AuthorProfile/Source as authoritative identity/provenance'
require_go_struct_contains internal/api/dto/task_browser.go TaskActivityEvent 'AuthorProfile\s+\*AuthorProfile\s+`json:"authorProfile,omitempty"`' 'browser task activity DTO must carry canonical AuthorProfile'
require_go_struct_contains internal/api/dto/task_browser.go TaskActivityEvent 'Source\s+\*ContributionSource\s+`json:"source,omitempty"`' 'browser task activity DTO must carry canonical ContributionSource'
require_contains internal/api/task/task_contract.go 'Activity identity and provenance are AuthorProfile and Source' 'internal task activity contract must document AuthorProfile/Source as authoritative identity/provenance'
require_go_struct_contains internal/api/task/task_contract.go TaskActivityEvent 'AuthorProfile\s+\*AuthorProfile\s+`json:"authorProfile,omitempty"`' 'internal task activity contract must carry canonical AuthorProfile'
require_go_struct_contains internal/api/task/task_contract.go TaskActivityEvent 'Source\s+\*ContributionSource\s+`json:"source,omitempty"`' 'internal task activity contract must carry canonical ContributionSource'
require_contains internal/api/agent/task_contract.go 'Canonical task activity identity and' 'agent task activity contract must document AuthorProfile/Source as canonical identity/provenance'
require_go_struct_contains internal/api/agent/task_contract.go TaskActivityEvent 'AuthorProfile\s+\*AuthorProfile\s+`json:"authorProfile,omitempty"`' 'agent task activity contract must carry canonical AuthorProfile'
require_go_struct_contains internal/api/agent/task_contract.go TaskActivityEvent 'Source\s+\*ContributionSource\s+`json:"source,omitempty"`' 'agent task activity contract must carry canonical ContributionSource'
require_contains internal/store/taskboard/task_timeline_read_service.go 'event\.AuthorProfile[[:space:]]+=[[:space:]]+taskActivityAuthorProfile\(event\.ActorRef, actor\)' 'task typed activity read path must hydrate canonical AuthorProfile from the activity actor projection'
require_contains internal/store/taskboard/task_timeline_read_service.go 'event\.Source[[:space:]]+=[[:space:]]+taskActivityContributionSource\(event\)' 'task typed activity read path must hydrate canonical ContributionSource'
require_contains internal/store/tasktimeline/read_query.go 'service_members actor_service_member' 'task typed activity read path must hydrate service authors through service_members'
require_contains internal/store/tasktimeline/read_query.go 'service_definitions actor_service_definition' 'task typed activity read path must hydrate service author display through service_definitions'
require_contains internal/store/taskboard/actor_projection.go 'func taskActivityAuthorProfile' 'task typed activity author projection must be explicit and not reuse TaskActor as the source of truth'
require_contains internal/api/task/taskboard_adapter.go 'taskActivityCompatActorRefFromAuthorProfile\(authorProfile, event\.ActorRef\)' 'task API adapter must derive legacy activity actorRef from canonical AuthorProfile'
require_contains internal/api/task/taskboard_adapter.go 'source := contributionSourceFromTaskboard\(event\.Source\)' 'task API adapter must preserve task activity ContributionSource before deriving legacy fields'
require_contains internal/api/task/taskboard_adapter.go 'SourceKind:[[:space:]]+taskActivityCompatSourceKindFromContributionSource\(source, event\.SourceKind\)' 'task API adapter must derive legacy activity source kind from canonical ContributionSource'
require_contains internal/api/task/taskboard_adapter.go 'SourceMessageID:[[:space:]]+taskActivityCompatSourceMessageIDFromContributionSource\(source, event\.SourceMessageID\)' 'task API adapter must derive legacy activity source message id from canonical ContributionSource'
require_contains internal/api/task/taskboard_adapter.go 'SourceActivityID:[[:space:]]+taskActivityCompatSourceActivityIDFromContributionSource\(source, event\.SourceActivityID\)' 'task API adapter must derive legacy activity source activity id from canonical ContributionSource'
require_contains internal/api/task/dto_conversion.go 'taskboardTaskActivityCompatActorRefFromAuthorProfile\(authorProfile, event\.ActorRef\)' 'task DTO bridge must derive legacy activity actorRef from canonical AuthorProfile'
require_contains internal/api/task/dto_conversion.go 'source := taskboardContributionSourceFromDTO\(event\.Source\)' 'task DTO bridge must preserve task activity ContributionSource before deriving legacy fields'
require_contains internal/api/task/dto_conversion.go 'SourceKind:[[:space:]]+taskboardTaskActivityCompatSourceKindFromContributionSource\(source, event\.SourceKind\)' 'task DTO bridge must derive legacy activity source kind from canonical ContributionSource'
require_contains internal/api/task/dto_conversion.go 'SourceMessageID:[[:space:]]+taskboardTaskActivityCompatSourceMessageIDFromContributionSource\(source, event\.SourceMessageID\)' 'task DTO bridge must derive legacy activity source message id from canonical ContributionSource'
require_contains internal/api/task/dto_conversion.go 'SourceActivityID:[[:space:]]+taskboardTaskActivityCompatSourceActivityIDFromContributionSource\(source, event\.SourceActivityID\)' 'task DTO bridge must derive legacy activity source activity id from canonical ContributionSource'
require_contains internal/api/agent/taskboard_adapter.go 'taskActivityCompatActorRefFromAuthorProfile\(authorProfile, event\.ActorRef\)' 'agent task API adapter must derive legacy activity actorRef from canonical AuthorProfile'
require_contains internal/api/agent/taskboard_adapter.go 'source := contributionSourceFromTaskboard\(event\.Source\)' 'agent task API adapter must preserve task activity ContributionSource before deriving legacy fields'
require_contains internal/api/agent/taskboard_adapter.go 'SourceKind:[[:space:]]+taskActivityCompatSourceKindFromContributionSource\(source, event\.SourceKind\)' 'agent task API adapter must derive legacy activity source kind from canonical ContributionSource'
require_contains internal/api/agent/taskboard_adapter.go 'SourceMessageID:[[:space:]]+taskActivityCompatSourceMessageIDFromContributionSource\(source, event\.SourceMessageID\)' 'agent task API adapter must derive legacy activity source message id from canonical ContributionSource'
require_contains internal/api/agent/taskboard_adapter.go 'SourceActivityID:[[:space:]]+taskActivityCompatSourceActivityIDFromContributionSource\(source, event\.SourceActivityID\)' 'agent task API adapter must derive legacy activity source activity id from canonical ContributionSource'
require_contains internal/api/agent/dto_conversion.go 'taskboardTaskActivityCompatActorRefFromAuthorProfile\(authorProfile, event\.ActorRef\)' 'agent task DTO bridge must derive legacy activity actorRef from canonical AuthorProfile'
require_contains internal/api/agent/dto_conversion.go 'source := taskboardContributionSourceFromDTO\(event\.Source\)' 'agent task DTO bridge must preserve task activity ContributionSource before deriving legacy fields'
require_contains internal/api/agent/dto_conversion.go 'SourceKind:[[:space:]]+taskboardTaskActivityCompatSourceKindFromContributionSource\(source, event\.SourceKind\)' 'agent task DTO bridge must derive legacy activity source kind from canonical ContributionSource'
require_contains internal/api/agent/dto_conversion.go 'SourceMessageID:[[:space:]]+taskboardTaskActivityCompatSourceMessageIDFromContributionSource\(source, event\.SourceMessageID\)' 'agent task DTO bridge must derive legacy activity source message id from canonical ContributionSource'
require_contains internal/api/agent/dto_conversion.go 'SourceActivityID:[[:space:]]+taskboardTaskActivityCompatSourceActivityIDFromContributionSource\(source, event\.SourceActivityID\)' 'agent task DTO bridge must derive legacy activity source activity id from canonical ContributionSource'
require_contains internal/api/dto/converter/task.go 'taskActivityCompatActorRefFromAuthorProfile\(authorProfile, event\.ActorRef\)' 'browser DTO converter must derive legacy activity actorRef from canonical AuthorProfile'
require_contains internal/api/dto/converter/task.go 'source := ToContributionSource\(event\.Source\)' 'browser DTO converter must preserve task activity ContributionSource before deriving legacy fields'
require_contains internal/api/dto/converter/task.go 'SourceKind:[[:space:]]+taskActivityCompatSourceKindFromContributionSource\(source, event\.SourceKind\)' 'browser DTO converter must derive legacy activity source kind from canonical ContributionSource'
require_contains internal/api/dto/converter/task.go 'SourceMessageID:[[:space:]]+taskActivityCompatSourceMessageIDFromContributionSource\(source, event\.SourceMessageID\)' 'browser DTO converter must derive legacy activity source message id from canonical ContributionSource'
require_contains internal/api/dto/converter/task.go 'SourceActivityID:[[:space:]]+taskActivityCompatSourceActivityIDFromContributionSource\(source, event\.SourceActivityID\)' 'browser DTO converter must derive legacy activity source activity id from canonical ContributionSource'
require_ts_type_contains web/src/types/tasks.ts TaskActivityEvent 'authorProfile\?:\s+AuthorProfile' 'frontend task activity type must expose canonical AuthorProfile'
require_ts_type_contains web/src/types/tasks.ts TaskActivityEvent 'source\?:\s+MessageContributionSource' 'frontend task activity type must expose canonical ContributionSource'
require_contains web/src/agentteam/workspace/tasks/taskActivityActor.ts 'resolveMessageAuthorIdentity\(event\)' 'task activity actor UI helper must prefer canonical AuthorProfile'
require_contains web/src/agentteam/workspace/tasks/detail/TaskActivityTimeline.tsx 'resolveTaskActivityAuthorIdentity\(' 'task activity timeline rendering must use canonical activity author identity helper'
require_contains web/src/agentteam/workspace/tasks/detail/taskDetailAgentNavigation.ts 'resolveMessageAuthorIdentity\(event\)' 'task activity agent navigation must prefer canonical AuthorProfile'
require_contains web/src/agentteam/workspace/tasks/detail/taskDetailActivity.ts 'messageAuthorIdentityKey\(event\)' 'task activity coalescing must use canonical author identity key'
require_contains internal/store/taskboard/store_wrappers.go 'func \(store \*Store\) SendTaskScopeMessage\(ctx context\.Context, request SendTaskScopeMessageRequest\)' 'taskboard store must expose canonical task-scope message send entrypoint'
require_contains internal/store/taskboard/discussion.go 'func \(txs TxStore\) SendTaskScopeMessage' 'taskboard must own canonical SendTaskScopeMessage entrypoint'
require_contains internal/store/app/task_scope_message_write.go 'taskboard\.New\(store\.db\)\.InTx\(tx\)\.SendTaskScopeMessage' 'Human task-scope send must write its message inside the app fanout transaction'
require_contains internal/store/app/task_scope_agent_message_write.go 'taskboard\.New\(store\.db\)\.InTx\(tx\)\.SendTaskScopeMessage' 'Agent task-scope send must write its message inside the app fanout transaction'
require_contains internal/api/im/messages_handler.go 'h\.writer\.SendHumanMessage' 'workspace IM task-scope sends must use the atomic target-aware Human writer'
require_contains internal/api/agent/store_adapter.go 's\.Store\.SendAgentTaskScopeMessageWithFreshness' 'agent raw task-scope sends must use the atomic app writer'
require_no_matches \
  'production code must not expose the obsolete two-phase task discussion projection API' \
  'SendTaskScopeDiscussionFor(User|Agent)' \
  internal \
  --glob '*.go' \
  --glob '!*_test.go'
require_contains internal/api/agent/api_messages_test.go 'TestMessageHandlerRawTaskScopeSendRequiresMinDaemonVersionBeforeStore' 'agent raw task-scope message sends must be covered by the message API daemon-version gate'
require_contains internal/api/agent/routes_test.go 'TestRegisterRoutesOwnsAgentTaskRoutes' 'workspace-scoped Agent Runtime task routes must have an explicit route inventory'
require_file_absent internal/api/agent/api_tasks.go 'retired old-daemon task route handlers must stay removed'
require_file_absent internal/api/agent/api_tasks_test.go 'retired old-daemon task route tests must stay removed'
require_file_absent internal/api/agent/task_operations.go 'retired old-daemon task route operations must stay removed'
require_contains specs/task/task-timeline-messageization.md 'Version pinning / mutable external facts' 'task timeline spec must document current rolling compatibility versus message API version pinning'
require_contains internal/api/im/messages_handler_test.go 'TestMessagesCreateTaskScopeUsesAtomicHumanWriterAndAcceptsClientMessageID' 'workspace IM regression must cover Task clientMessageId through the atomic writer'
require_contains internal/api/task/api_parity.go '"taskAnnouncementAudit"' 'task announcement audit copy must carry structured provenance metadata'
require_contains internal/api/task/api_parity_test.go 'TestTaskParityCreateAnnouncementUsesFanout' 'task announcement must cover atomic source and Task-audit deliveries'
require_no_matches \
  'Task API production code must not bypass the atomic app writer for task discussions' \
  '\.SendTaskScopeMessage' \
  internal/api/task \
  --glob '*.go' \
  --glob '!*_test.go'
require_file_absent internal/task/boardclient/client.go 'retired remote task service client must stay removed'
require_file_absent internal/task/boardclient/http_client.go 'retired remote task service HTTP driver must stay removed'
require_contains web/src/types/tasks.ts 'type TaskTimelineEntry' 'frontend task model must expose the unified timeline entry type'
require_contains web/src/agentteam/workspace/tasks/detail/TaskActivityTimeline.tsx 'detail\.timeline' 'task detail UI must render from the unified timeline DTO'
require_contains web/src/agentteam/workspace/tasks/detail/TaskActivityTimeline.tsx 'resolveMessageAuthorIdentity\(entry\)' 'task discussion timeline must render author identity from message AuthorProfile'

require_no_matches \
  'task discussion production read paths must not use legacy task discussion physical tables as runtime source of truth' \
  'task_discussion_entries|task_discussion_reactions|task_discussions' \
  internal/store/taskboard/discussion_page.go \
  internal/store/taskboard/task_scope_message.go \
  internal/api/task \
  --glob '*.go' \
  --glob '!*_test.go'

require_no_matches \
  'frontend task detail production code must not read legacy discussion createdBy fields; CreatedBy is compatibility wire only' \
  'entry\.createdBy\.(id|name|type)|latestDiscussionEntry\.createdBy\.(id|name|type)' \
  web/src/agentteam/workspace/tasks/detail/TaskActivityTimeline.tsx \
  web/src/agentteam/workspace/tasks/taskAgentLiveMonitor.ts \
  --glob '*.ts' \
  --glob '*.tsx'

require_no_matches \
  'frontend task typed activity render/monitor code must not use legacy actorDisplayName or actorRef as the primary identity' \
  'item\.event\.actorDisplayName|entry\.event\.actorDisplayName|resolveTaskActivityActorDisplayName\(' \
  web/src/agentteam/workspace/tasks/detail/TaskActivityTimeline.tsx \
  web/src/agentteam/workspace/tasks/taskAgentLiveMonitor.ts \
  --glob '*.ts' \
  --glob '*.tsx'

require_no_matches \
  'frontend task typed activity coalescing must not compare legacy actorRef directly' \
  'prev\.event\.actorRef === entry\.event\.actorRef' \
  web/src/agentteam/workspace/tasks/detail/taskDetailActivity.ts \
  --glob '*.ts'

require_contains internal/store/taskboard/actor_projection.go 'type TaskPrincipalProfile = message\.AuthorProfile' 'task body identity must use the same AuthorProfile substrate as message/reaction/activity identity'
require_go_struct_contains internal/store/taskboard/service.go Task 'AssigneeProfile\s+\*TaskPrincipalProfile' 'task store model must expose one canonical assignee profile'
require_go_struct_contains internal/store/taskboard/service.go Task 'AssignedAt\s+\*time\.Time' 'task store model must expose canonical assignment time'
require_go_struct_contains internal/store/taskboard/service.go Task 'CreatedByProfile\s+\*TaskPrincipalProfile' 'task store model must preserve creator identity separately from assignee'
require_go_struct_contains internal/store/taskboard/service.go Task 'Source\s+message\.ContributionSource' 'task store model must expose canonical task provenance source'
require_go_struct_contains internal/store/taskboard/service.go TaskView 'AssigneeProfile\s+\*TaskPrincipalProfile' 'compact task views must expose the canonical assignee profile'
require_contains internal/store/taskboard/task_scan.go 'taskPrincipalProfileFromProjection\("assignee"' 'task reads must hydrate the canonical assignee profile'
require_contains internal/store/taskboard/task_scan.go 'taskLegacyCreatedByFromPrincipalProfile\(createdProfile' 'task reads may derive only creator compatibility fields from canonical creator profile'
require_contains internal/store/taskboard/task_scan.go 'task\.Source[[:space:]]+=[[:space:]]+taskSourceContributionSource\(task\.SourceThread, task\.CreatedByProfile\)' 'task store read path must derive canonical task provenance source from source thread and creator profile'
require_contains internal/store/taskboard/source_provenance.go 'ContributionSourceRefTypeMessage' 'task source provenance must use message ContributionSource refs for source-thread anchors'
require_contains internal/store/taskboard/user_tasks.go 'service_members created_service_member' 'task creator identity read path must hydrate service principals through service_members'
require_contains internal/store/taskboard/user_tasks.go 'service_definitions created_service_definition' 'task creator identity read path must hydrate service display through service_definitions'

require_go_struct_contains internal/api/dto/task_browser.go Task 'AssigneeProfile\s+\*AuthorProfile\s+`json:"assigneeProfile,omitempty"`' 'browser Task DTO must expose one canonical assignee profile'
require_go_struct_contains internal/api/dto/task_browser.go Task 'AssignedAt\s+\*time\.Time\s+`json:"assignedAt,omitempty"`' 'browser Task DTO must expose canonical assignment time'
require_go_struct_contains internal/api/dto/task_browser.go TaskCreateInputRequest 'AssigneeRef\s+string\s+`json:"assigneeRef,omitempty"`' 'browser Task create input must accept canonical assigneeRef'
require_go_struct_contains internal/api/dto/task_browser.go TaskUpdateRequest 'AssigneeRef\s+\*string\s+`json:"assigneeRef,omitempty"`' 'browser Task update must distinguish omitted assigneeRef from clear'
require_go_struct_contains internal/api/dto/task_browser.go RelatedTaskRow 'AssigneeProfile\s+\*AuthorProfile\s+`json:"assigneeProfile,omitempty"`' 'browser related Task rows must expose canonical assignee profile'
require_go_struct_contains internal/api/dto/task_browser.go Task 'Source\s+\*ContributionSource\s+`json:"source,omitempty"`' 'browser task DTO must expose canonical task provenance source'
require_go_struct_contains internal/api/dto/task_browser.go RelatedTaskRow 'Source\s+\*ContributionSource\s+`json:"source,omitempty"`' 'browser related task rows must expose canonical task provenance source'
require_go_struct_contains internal/api/task/task_contract.go Task 'AssigneeProfile\s+\*AuthorProfile\s+`json:"assigneeProfile,omitempty"`' 'human Task contract must expose canonical assignee profile'
require_go_struct_contains internal/api/task/task_contract.go TaskCreateInput 'AssigneeRef\s+string\s+`json:"assigneeRef,omitempty"`' 'human Task create contract must accept canonical assigneeRef'
require_go_struct_contains internal/api/task/task_contract.go TaskUpdatePatch 'AssigneeRef\s+\*string\s+`json:"assigneeRef,omitempty"`' 'human Task update contract must accept canonical assigneeRef'
require_go_struct_contains internal/api/agent/task_contract.go Task 'AssigneeProfile\s+\*AuthorProfile\s+`json:"assigneeProfile,omitempty"`' 'Agent Task contract must expose canonical assignee profile'
require_go_struct_contains internal/api/agent/task_contract.go TaskCreateInput 'AssigneeRef\s+string\s+`json:"assigneeRef,omitempty"`' 'Agent Task create contract must accept canonical assigneeRef'
require_go_struct_contains internal/api/agent/task_contract.go TaskUpdatePatch 'AssigneeRef\s+\*string\s+`json:"assigneeRef,omitempty"`' 'Agent Task update contract must accept canonical assigneeRef'
require_contains internal/api/dto/converter/task.go 'AssigneeProfile:[[:space:]]+ToTaskPrincipalProfile\(task\.AssigneeProfile\)' 'browser Task converter must preserve canonical assignee profile'
require_contains internal/api/task/taskboard_adapter.go 'AssigneeProfile:[[:space:]]+taskPrincipalProfileFromTaskboard\(task\.AssigneeProfile\)' 'human Task adapter must preserve canonical assignee profile'
require_contains internal/api/agent/taskboard_adapter.go 'AssigneeProfile:[[:space:]]+taskPrincipalProfileFromTaskboard\(task\.AssigneeProfile\)' 'Agent Task adapter must preserve canonical assignee profile'
require_contains internal/api/task/dto_conversion.go 'AssigneeProfile:[[:space:]]+taskboardTaskPrincipalProfileForDTO\(task\.AssigneeProfile\)' 'human Task reverse bridge must preserve canonical assignee profile'
require_contains internal/api/agent/dto_conversion.go 'AssigneeProfile:[[:space:]]+taskboardTaskPrincipalProfileForDTO\(task\.AssigneeProfile\)' 'Agent Task reverse bridge must preserve canonical assignee profile'
require_contains internal/store/taskboard/related_tasks.go 'Source:[[:space:]]+task\.Source' 'related task rows must preserve canonical task provenance source'
require_contains internal/api/dto/converter/task.go 'Source:[[:space:]]+ToContributionSource\(task\.Source\)' 'browser task DTO converter must map canonical task provenance source from store'
require_ts_type_contains web/src/types/tasks.ts Task 'assigneeProfile\?:\s+AuthorProfile' 'frontend Task type must expose canonical assigneeProfile'
require_ts_type_contains web/src/types/tasks.ts Task 'assignedAt\?:\s+string' 'frontend Task type must expose canonical assignedAt'
require_ts_type_contains web/src/types/tasks.ts TaskUpdatePatch 'assigneeRef\?:\s+string \| null' 'frontend Task update must represent explicit unassign with null'
require_ts_type_contains web/src/types/tasks.ts TaskCreateInput 'assigneeRef\?:\s+string' 'frontend Task create input must accept canonical assigneeRef'
require_contains web/src/agentteam/workspace/tasks/taskPrincipalIdentity.ts 'taskAssigneeIdentity' 'frontend Task identity helper must own canonical assignee identity'
require_contains web/src/agentteam/workspace/tasks/identity.ts 'taskAssigneeIdentity' 'canonical assignee identity must have a narrow cross-domain export'
require_contains web/src/agentteam/workspace/tasks/detail/TaskPropertySidebar.tsx 'taskAssigneeIdentity\(detail\.task\)' 'Task property sidebar must render canonical assignee identity'
require_contains web/src/agentteam/workspace/tasks/board/TaskBoardCard.tsx 'taskAssigneeIdentity\(task\)' 'Task board card must render canonical assignee identity'
require_contains web/src/agentteam/workspace/conversation/components/reply-thread-panel/threadTaskActivity.ts 'relatedTaskAssigneeIdentity\(task\)' 'thread Task activity must render canonical related-task assignee identity'
require_contains web/src/agentteam/workspace/conversation/components/reply-thread-panel/threadTaskActivity.ts 'relatedTaskAssigneeIdentity\(task\)' 'thread related Task rows must render canonical assignee identity'
require_go_struct_contains desktop/internal/api/exec/task.go taskSummary 'AssigneeProfile\s+\*taskActorProfile\s+`json:"assigneeProfile,omitempty"`' 'Desktop task exec results must expose canonical assigneeProfile'
require_go_struct_contains desktop/internal/api/exec/task.go taskUpdateRequest 'Assignee\s+\*string\s+`json:"assignee,omitempty"`' 'Desktop task exec update must distinguish omitted assignee from explicit unassign'
require_contains desktop/internal/promptcompiler/task_data.go 'promptMapValue\(raw, "assigneeProfile"\)' 'Desktop typed task prompt decoder must render canonical Task assigneeProfile'
require_contains desktop/internal/wire/mcpformat/format.go 'task\["assigneeProfile"\]' 'Desktop MCP receive output must render canonical Task assigneeProfile'
require_contains desktop/prompts/platform/agentteam/task_boards.md 'status, assignee, blocker' 'Desktop Task board instructions must describe canonical Task assignee responsibility'
require_no_matches \
  'Desktop Task exec/prompt/wire surfaces must not expose removed responsibility aliases or Task-row reviewer' \
  'suggestedToYou|suggestedAssignee|currentAgent|claimedBy|task\["reviewer"\]|ReviewerLabel|"reviewer:"|status, reviewer' \
  desktop/internal/api/exec/task.go \
  desktop/internal/promptcompiler/text_task.go \
  desktop/internal/wire/mcpformat/format.go \
  desktop/prompts/wire/core.md \
  desktop/prompts/platform/agentteam/task_boards.md

require_contains internal/tidb/bootstrap.sql '`assignee_member_id` varchar\(255\) DEFAULT NULL' 'fresh schema must contain canonical Task assignee member FK column'
require_contains internal/tidb/bootstrap.sql '`assigned_at` timestamp NULL DEFAULT NULL' 'fresh schema must contain canonical Task assignment time'
require_contains internal/tidb/bootstrap.sql 'CONSTRAINT `fk_tasks_assignee_member` FOREIGN KEY \(`assignee_member_id`\) REFERENCES `members` \(`id`\)' 'fresh schema must constrain canonical Task assignee to workspace members'
require_contains internal/tidb/bootstrap.sql '`claimed_by_member_id` varchar\(255\) DEFAULT NULL' 'rolling Task schema must retain the legacy claim member column until the contract release'
require_contains internal/tidb/bootstrap.sql '`claimed_by_ref` varchar\(255\) DEFAULT NULL' 'rolling Task schema must retain the legacy claim ref column until the contract release'
require_contains internal/tidb/bootstrap.sql '`current_agent_ref` varchar\(255\) DEFAULT NULL' 'rolling Task schema must retain the legacy current-agent column until the contract release'
require_contains internal/tidb/bootstrap.sql '`suggested_assignee_ref` varchar\(255\) NOT NULL DEFAULT' 'rolling Task schema must retain the legacy suggested-assignee column until the contract release'
require_contains internal/tidb/bootstrap.sql '`reviewer_ref` varchar\(255\) NOT NULL DEFAULT' 'rolling Task schema must retain the legacy Task-row reviewer column until the contract release'
require_contains internal/tidb/bootstrap.sql 'CONSTRAINT `fk_tasks_claimed_by_member` FOREIGN KEY \(`claimed_by_member_id`\) REFERENCES `members` \(`id`\)' 'rolling Task schema must retain the legacy claim FK until old binaries are drained'
require_no_matches \
  'expand migration must not contract legacy Task responsibility columns, indexes, or FKs while old binaries are live' \
  'DROP COLUMN IF EXISTS `(claimed_by_member_id|claimed_by_ref|claimed_at|current_agent_ref|suggested_assignee_ref|reviewer_ref)`|DROP INDEX IF EXISTS `tasks_(claimed|current_agent|suggested_assignee|reviewer)|DROP FOREIGN KEY `fk_tasks_claimed_by_member`' \
  internal/tidb/migrations/00289_task_canonical_assignee.sql
require_contains internal/store/taskcompat/assignee.go 'func EffectiveAssigneeRefSQL' 'rolling Task readers must share one frozen legacy-to-canonical assignee projection'
require_contains internal/store/taskcompat/assignee.go 'claimed_by_ref' 'rolling Task assignee projection must preserve claimed-by-ref precedence'
require_contains internal/store/taskcompat/assignee.go 'claimed_by_member_id' 'rolling Task assignee projection must preserve claimed-member precedence'
require_contains internal/store/taskcompat/assignee.go 'current_agent_ref' 'rolling Task assignee projection must preserve current-agent precedence'
require_contains internal/store/taskcompat/assignee.go 'suggested_assignee_ref' 'rolling Task assignee projection must preserve suggested-assignee precedence'
require_contains internal/store/taskcompat/assignee.go 'reviewer_ref' 'rolling Task assignee projection must preserve Task-row reviewer precedence'
require_contains internal/store/taskcompat/assignee.go 'func EffectiveAssignedAtSQL' 'rolling Task reads must project an effective assignedAt for old-node mutations'
require_contains internal/store/taskboard/user_tasks.go 'taskcompat\.EffectiveAssigneeRefSQL\("t"\)' 'Task body reads must use the mixed-version assignee projection'
require_contains internal/store/taskboard/query_builders.go 'taskcompat\.EffectiveAssigneeRefSQL\("t"\)' 'Task browse filters must use the mixed-version assignee projection'
require_contains internal/store/taskboard/related_tasks.go 'taskcompat\.EffectiveAssigneeRefSQL\("t"\)' 'related Agent Task filters must use the mixed-version assignee projection'
require_contains internal/store/taskboard/search.go 'taskcompat\.EffectiveAssigneeRefSQL\("t"\)' 'Task profile search must use the mixed-version assignee projection'
require_contains internal/store/tasktimeline/attention.go 'taskcompat\.EffectiveAssigneeRefSQL\("t"\)' 'Task attention must wake the effective mixed-version assignee'
require_contains internal/store/app/app_channel_agent_task_summaries.go 'taskcompat\.EffectiveAssigneeRefSQL\("t"\)' 'Agent Task summaries must use the mixed-version assignee projection'
require_contains internal/store/taskboard/service.go 'claimed_by_member_id' 'new Task creation must dual-write the legacy claim projection'
require_contains internal/store/taskboard/detail_mutation.go 'claimed_by_member_id = CASE WHEN \$14' 'Task assignee PATCH must dual-write the legacy claim projection'
require_contains internal/store/taskboard/detail_mutation.go 'current_agent_ref = CASE WHEN \$14 THEN NULL' 'Task assignee PATCH must clear lower-precedence legacy responsibility fields'
require_contains internal/store/taskboard/agent_tasks.go 'claimed_by_member_id = \$3' 'Agent claim must dual-write the legacy claim projection'
require_contains internal/store/taskboard/agent_tasks.go "suggested_assignee_ref = ''" 'Agent claim/unclaim must clear lower-precedence legacy responsibility fields'
require_contains internal/store/taskboard/user_tasks.go 'claimed_by_member_id = \$2' 'Human claim must dual-write the legacy claim projection'
require_not_contains_multiline internal/store/taskboard/detail_review.go 'UPDATE\s+tasks\s+SET(?:(?!WHERE).)*(assignee_member_id|claimed_by_member_id|claimed_by_ref|current_agent_ref|suggested_assignee_ref|reviewer_ref)' 'Review Request reviewer must not overwrite Task responsibility columns'
require_no_matches \
  'Review Request reviewer must not emit assigned activity' \
  'taskEventAssigned' \
  internal/store/taskboard/detail_review.go
require_contains internal/store/taskboard/review_integration_test.go 'requestedReview\.Task\.AssigneeProfile' 'requesting review must preserve the existing Task assignee'
require_contains internal/store/taskboard/review_integration_test.go 'reassignedReview\.Task\.AssigneeProfile' 'reassigning a Review Request reviewer must preserve the existing Task assignee'
require_contains internal/store/taskboard/review_integration_test.go 'assignedEventsBeforeReview' 'review request and reviewer reassignment must not emit assigned activity'
require_go_struct_contains internal/store/taskboard/types.go TaskCollaborator 'MemberProfile\s+\*TaskPrincipalProfile' 'task collaborator store read model must expose canonical member profile'
require_go_struct_contains internal/store/taskboard/types.go TaskCollaborator 'AddedByProfile\s+\*TaskPrincipalProfile' 'task collaborator store read model must expose canonical addedBy profile'
require_go_struct_contains internal/store/taskboard/types.go TaskCollaborator 'RemovedByProfile\s+\*TaskPrincipalProfile' 'task collaborator store read model must expose canonical removedBy profile'
require_go_struct_contains internal/store/taskboard/types.go AgentCollaboratorSummary 'MemberProfile\s+\*TaskPrincipalProfile' 'agent collaborator summary store model must expose canonical member profile'
require_go_struct_contains internal/store/taskboard/types.go EffectiveTaskCollaborator 'MemberProfile\s+\*TaskPrincipalProfile' 'effective collaborator store model must expose canonical member profile'
require_go_struct_contains internal/store/taskboard/contract.go ListAgentCollaboratorsResponse 'OwnerProfile\s+\*TaskPrincipalProfile' 'agent collaborator list response must expose canonical owner profile'
require_go_struct_contains internal/store/taskboard/contract.go ListEffectiveTaskCollaboratorsResponse 'OwnerProfile\s+\*TaskPrincipalProfile' 'effective collaborator list response must expose canonical owner profile'
require_go_struct_contains internal/api/dto/task_collaborators.go TaskCollaborator 'MemberProfile\s+\*AuthorProfile\s+`json:"memberProfile,omitempty"`' 'task collaborator browser DTO must expose canonical member profile'
require_go_struct_contains internal/api/dto/task_collaborators.go TaskCollaborator 'AddedByProfile\s+\*AuthorProfile\s+`json:"addedByProfile,omitempty"`' 'task collaborator browser DTO must expose canonical addedBy profile'
require_go_struct_contains internal/api/dto/task_collaborators.go TaskCollaborator 'RemovedByProfile\s+\*AuthorProfile\s+`json:"removedByProfile,omitempty"`' 'task collaborator browser DTO must expose canonical removedBy profile'
require_go_struct_contains internal/api/dto/task_collaborators.go AgentCollaboratorSummary 'MemberProfile\s+\*AuthorProfile\s+`json:"memberProfile,omitempty"`' 'agent collaborator browser DTO must expose canonical member profile'
require_go_struct_contains internal/api/dto/task_collaborators.go EffectiveTaskCollaborator 'MemberProfile\s+\*AuthorProfile\s+`json:"memberProfile,omitempty"`' 'effective collaborator browser DTO must expose canonical member profile'
require_go_struct_contains internal/api/dto/task_collaborators.go ListAgentCollaboratorsResponse 'OwnerProfile\s+\*AuthorProfile\s+`json:"ownerProfile,omitempty"`' 'agent collaborator browser DTO must expose canonical owner profile'
require_go_struct_contains internal/api/dto/task_collaborators.go ListEffectiveTaskCollaboratorsResponse 'OwnerProfile\s+\*AuthorProfile\s+`json:"ownerProfile,omitempty"`' 'effective collaborator browser DTO must expose canonical owner profile'
require_go_struct_contains internal/store/taskboard/contract.go PrivateAgentMentionCandidate 'OwnerProfile\s+\*TaskPrincipalProfile' 'private agent mention candidate store contract must expose canonical owner profile'
require_go_struct_contains internal/api/dto/task_collaborators.go PrivateAgentMentionCandidate 'OwnerProfile\s+\*AuthorProfile\s+`json:"ownerProfile,omitempty"`' 'private agent mention candidate browser DTO must expose canonical owner profile'
require_go_struct_contains internal/store/message/projections.go ChannelPrivateAgentSummary 'OwnerProfile\s+AuthorProfile' 'channel private agent summary store model must expose canonical owner profile'
require_go_struct_contains internal/api/dto/message_responses.go ChannelPrivateAgentSummary 'OwnerProfile\s+\*AuthorProfile\s+`json:"ownerProfile,omitempty"`' 'channel private agent summary DTO must expose canonical owner profile'
require_contains internal/store/taskboard/collaborator_queries.go 'taskPrincipalProfileFromProjection\("task collaborator member"' 'task collaborator runtime scan must hydrate canonical member profile'
require_contains internal/store/taskboard/collaborator_queries.go 'taskActorCompatFromPrincipalProfile\(memberProfile, member\)' 'task collaborator runtime scan must derive legacy member actor from canonical profile'
require_contains internal/store/taskboard/collaborator_queries.go 'taskPrincipalProfileFromProjection\("task collaborator addedBy"' 'task collaborator runtime scan must hydrate canonical addedBy profile'
require_contains internal/store/taskboard/collaborator_queries.go 'taskActorCompatFromPrincipalProfile\(addedByProfile, addedBy\)' 'task collaborator runtime scan must derive legacy addedBy actor from canonical profile'
require_contains internal/store/taskboard/collaborator_queries.go 'taskPrincipalProfileFromProjection\("task collaborator removedBy"' 'task collaborator runtime scan must hydrate canonical removedBy profile'
require_contains internal/api/dto/converter/task.go 'MemberProfile:[[:space:]]+memberProfile' 'task collaborator DTO converter must preserve canonical member profile'
require_contains internal/api/dto/converter/task.go 'Member:[[:space:]]+taskCompatActorFromAuthorProfile\(memberProfile, collaborator\.Member\)' 'task collaborator DTO converter must derive legacy member actor from canonical profile'
require_contains internal/api/dto/converter/task.go 'OwnerProfile:[[:space:]]+ownerProfile' 'collaborator list DTO converters must preserve canonical owner profile'
require_contains internal/store/taskboard/private_agent_mention_candidates.go 'taskPrincipalProfileFromProjection\("private agent mention owner"' 'private agent mention runtime scan must hydrate canonical owner profile'
require_contains internal/store/taskboard/private_agent_mention_candidates.go 'privateAgentMentionCandidateDeriveOwnerCompat\(&candidate\)' 'private agent mention runtime scan must derive legacy owner fields from canonical owner profile'
require_contains internal/api/dto/converter/task.go 'OwnerProfile:[[:space:]]+ownerProfile' 'private agent mention DTO converter must preserve canonical owner profile'
require_contains internal/api/dto/converter/task.go 'privateAgentMentionOwnerCompatFromAuthorProfile\(ownerProfile, candidate\)' 'private agent mention DTO converter must derive legacy owner fields from canonical owner profile'
require_contains internal/store/message/projections.go 'privateChannelAgentOwnerProfile\(ownerMemberID, ownerUserID, ownerDisplayName\)' 'channel private agent summary scan must hydrate canonical owner profile from local projection variables'
require_contains internal/store/message/projections.go 'derivePrivateChannelAgentOwnerCompat\(&agent\)' 'channel private agent summary scan must derive legacy owner fields from canonical owner profile'
require_contains internal/api/dto/converter/message.go 'ownerProfile := ToAuthorProfile\(agent\.OwnerProfile\)' 'channel private agent DTO converter must preserve canonical owner profile'
require_contains internal/api/dto/converter/message.go 'channelPrivateAgentOwnerCompatFromAuthorProfile\(ownerProfile, agent\)' 'channel private agent DTO converter must derive legacy owner fields from canonical owner profile'
require_contains web/src/agentteam/workspace/conversation/state/peopleBoundaries.ts 'profileName\(collaborator\.memberProfile, collaborator\.member\)' 'People/effective collaborator UI must render member names through canonical profile helper'
require_contains_multiline web/src/agentteam/workspace/agent-collaborators/AgentCollaboratorsRouteSections.tsx 'collaboratorProfileName\(\s*summary\.memberProfile,\s*summary\.member,\s*t\s*\)' 'Agent collaborator UI must render member names through canonical profile helper'
require_contains web/src/agentteam/api/tasks/collaboration.ts 'ownerProfile:[[:space:]]+response\?\.ownerProfile' 'Agent collaborator Web normalizer must preserve canonical owner profile'
require_ts_type_contains web/src/types/tasks.ts PrivateAgentMentionCandidate 'ownerProfile\?:\s+AuthorProfile' 'frontend private agent mention candidate type must expose canonical owner profile'
require_ts_type_contains web/src/types/core.ts ChannelPrivateAgentSummary 'ownerProfile\?:\s+AuthorProfile' 'frontend channel private agent summary type must expose canonical owner profile'
require_contains web/src/agentteam/workspace/shared/mentionCandidates.ts 'privateAgentOwnerIdentity\(candidate\)' 'private agent mention UI must resolve candidate owner identity through canonical profile helper'
require_contains web/src/agentteam/workspace/shared/mentionCandidates.ts 'privateAgentOwnerIdentity\(privateAgentSummary\)' 'channel private agent mention UI must resolve owner identity through canonical profile helper'
require_contains web/src/agentteam/api/core.ts 'member\.ownerProfile\?\.memberId\?\.trim\(\) \|\| member\.ownerMemberId\.trim\(\)' 'channel private agent normalization must key owner-scoped rows by canonical owner profile member id first'
require_no_matches \
  'Task public contracts must not reintroduce removed responsibility or create-mode symbols' \
  'CurrentAgent(Profile|Ref)?|ClaimedBy(Profile|Ref|Type|ID|Id|Name)?|SuggestedAssignee(Profile|Ref)?|currentAgent(Profile|Ref)?|claimedBy(Profile|Ref|Type|ID|Id|Name)?|suggestedAssignee(Profile|Ref)?|claimed_at|claimed_by_(member_id|ref)|current_agent_ref|suggested_assignee_ref|CreateMode|createMode|create_mode' \
  internal/store/taskboard/contract.go \
  internal/api/task/task_contract.go \
  internal/api/agent/task_contract.go \
  internal/api/dto/task_browser.go \
  --glob '*.go' \
  --glob '!*_test.go'

require_no_matches \
  'frontend Task production code must not reintroduce removed responsibility/create-mode fields' \
  'currentAgent(Profile|Ref)?|claimedBy(Profile|Ref|Type|Id|ID|Name)?|claimedAt|suggestedAssignee(Profile|Ref)?|TaskCreateMode|createMode' \
  web/src/types/tasks.ts \
  web/src/agentteam/api/tasks \
  web/src/agentteam/workspace/tasks \
  web/src/agentteam/workspace/conversation/components/reply-thread-panel \
  web/src/agentteam/workspace/inbox \
  --glob '*.ts' \
  --glob '*.tsx' \
  --glob '!*.test.ts' \
  --glob '!*.test.tsx'

require_no_matches \
  'Task UI must not expose Task-row reviewer or suggested-assignee properties; Review Request reviewer remains separate' \
  'reviewerOptions|taskReviewerIdentity|property_reviewer|taskSuggestedAssigneeIdentity' \
  web/src/agentteam/workspace/tasks/detail/TaskPropertySidebar.tsx \
  web/src/agentteam/workspace/tasks/board/TaskBoardCard.tsx \
  --glob '*.ts' \
  --glob '*.tsx'

require_contains_multiline web/src/agentteam/workspace/tasks/detail/TaskMembersSidePanel.tsx 'selfMutation\.mutate\(\s*watching\s*\?\s*"mute"\s*:\s*"active"\s*\)' 'Human Task Watch must expose active'
require_contains_multiline web/src/agentteam/workspace/tasks/detail/TaskMembersSidePanel.tsx 'selfMutation\.mutate\(\s*watching\s*\?\s*"mute"\s*:\s*"active"\s*\)' 'Human Task unwatch must expose mute'
require_contains_multiline web/src/agentteam/workspace/tasks/detail/TaskMembersSidePanel.tsx 'wakeState:\s*watching\s*\?\s*"no-wake"\s*:\s*"auto-wake"' 'Agent Task Watch must expose auto-wake'
require_contains_multiline web/src/agentteam/workspace/tasks/detail/TaskMembersSidePanel.tsx 'wakeState:\s*watching\s*\?\s*"no-wake"\s*:\s*"auto-wake"' 'Agent Task Only-at mode must expose no-wake'
require_no_matches \
  'Task notification UI must not expose legacy quiet state' \
  '"quiet"' \
  web/src/agentteam/workspace/tasks/detail \
  --glob '*.ts' \
  --glob '*.tsx' \
  --glob '!*.test.ts' \
  --glob '!*.test.tsx'
require_contains web/src/agentteam/workspace/inbox/state/inboxFilters.ts '"task"' 'Inbox filters must expose a Task category'
require_contains web/src/agentteam/workspace/inbox/components/ConversationRow.tsx 'item\.kind === "task"' 'Inbox must render Task rows explicitly'
require_no_matches \
  'task collaborator DTO converter must not map legacy TaskActor directly as source of truth' \
  'Member:[[:space:]]+\*ToTaskActor\(&collaborator\.Member\)|AddedBy:[[:space:]]+\*ToTaskActor\(&collaborator\.AddedBy\)|Member:[[:space:]]+\*ToTaskActor\(&summary\.Member\)|Owner:[[:space:]]+\*ToTaskActor\(&response\.Owner\)' \
  internal/api/dto/converter/task.go \
  --glob '*.go' \
  --glob '!*_test.go'

require_no_multiline_matches \
  'task collaborator production UI must not render legacy TaskActor aliases directly as source of truth' \
  'actorName\s*\(\s*(collaborator\.(member|addedBy|removedBy)|summary\.member)\s*\)|actorAvatarType\s*\(\s*summary\.member\s*\)' \
  web/src/agentteam/workspace/conversation/state/peopleBoundaries.ts \
  web/src/agentteam/workspace/AgentCollaboratorsRoute.tsx

require_no_matches \
  'private agent mention runtime scan must not write legacy owner fields directly from SQL as source of truth' \
  '&candidate\.Owner(MemberID|UserID|Name|DisplayName)' \
  internal/store/taskboard/private_agent_mention_candidates.go \
  --glob '*.go' \
  --glob '!*_test.go'

require_no_matches \
  'private agent mention DTO converter must not map legacy owner fields directly as source of truth' \
  'Owner(MemberID|UserID|Name|DisplayName):[[:space:]]+candidate\.Owner(MemberID|UserID|Name|DisplayName)' \
  internal/api/dto/converter/task.go \
  --glob '*.go' \
  --glob '!*_test.go'

require_no_matches \
  'channel private agent summary runtime scan must not write legacy owner fields directly from SQL as source of truth' \
  '&agent\.Owner(MemberID|UserID|Name|DisplayName)' \
  internal/store/message/projections.go \
  --glob '*.go' \
  --glob '!*_test.go'

require_no_matches \
  'channel private agent DTO converter must not map legacy owner fields directly as source of truth' \
  'Owner(MemberID|UserID|Name|DisplayName):[[:space:]]+agent\.Owner(MemberID|UserID|Name|DisplayName)' \
  internal/api/dto/converter/message.go \
  --glob '*.go' \
  --glob '!*_test.go'

require_no_multiline_matches \
  'private agent mention UI must not pass legacy owner display names directly to mention detail' \
  'privateAgentMentionDetail\s*\([^)]*(candidate|privateAgentSummary)\.ownerDisplayName' \
  web/src/agentteam/workspace/shared/mentionCandidates.ts

require_no_matches \
  'Conversation/Inbox production UI must not read legacy task metadata compact actors directly' \
  '\btask[[:space:]]*(!|\?)?[[:space:]]*\.[[:space:]]*(currentAgent|claimedBy)[[:space:]]*(!|\?)?[[:space:]]*\.[[:space:]]*(id|name|type)' \
  web/src/agentteam/workspace/inbox \
  web/src/agentteam/workspace/conversation \
  --glob '*.ts' \
  --glob '*.tsx' \
  --glob '!*.test.ts' \
  --glob '!*.test.tsx'

require_no_matches \
  'frontend task detail code must not fetch legacy split discussion/activity timeline pages' \
  '\bgetTaskDiscussion\b|\bgetTaskActivity\b|\bappendTaskDetailDiscussionPage\b|\bappendTaskDetailActivityPage\b|\bTaskDiscussionPage\b|\bTaskActivityPage\b|taskQueryKeys\.discussion' \
  web/src/agentteam \
  --glob '*.ts' \
  --glob '*.tsx'

require_no_matches \
  'browser task routes must not expose legacy split activity/discussion list endpoints or task-native discussion append routes' \
  'GET \"/tasks/:taskId/(discussion|activity)\"|\.GET\(\"/tasks/:taskId/(discussion|activity)\"|POST \"/tasks/:taskId/discussion\"|\.POST\(\"/tasks/:taskId/discussion\"|TaskDiscussionRequest|TaskDiscussionResponse|DiscussionHandler' \
  internal/api/task \
  --glob '*.go'

require_no_matches \
  'current implementation docs must not list the removed task-native browser discussion append route as implemented' \
  '^  - `POST /api/tasks/:taskId/discussion`$' \
  docs/tech_design.md

require_no_matches \
  'browser task routes must not expose task-native discussion edit/delete/reaction mutation endpoints; task discussion reactions use /api/messages/:messageId/reactions' \
  '/tasks/:taskId/discussion/:entryId|TaskDiscussion(Edit|Reaction)Request|handler\.(Edit|AddReaction|RemoveReaction)' \
  internal/api/task \
  --glob '*.go' \
  --glob '!*_test.go'

require_no_matches \
  'server-side task timeline reads must not expose legacy split GetTaskDiscussion/GetTaskActivity entrypoints' \
  '\bGetTask(Discussion|Activity)\s*\(' \
  internal/api/task/ports.go \
  internal/api/task/task_contract.go \
  internal/api/task/taskboard_adapter.go \
  internal/store/taskboard/store_wrappers.go \
  internal/store/taskboard/detail_activity_read.go \
  internal/store/taskboard/discussion.go \
  internal/store/taskboard/contract.go

require_no_matches \
  'server-side task discussion mutation ports must not expose task-native discussion first-class writers; canonical browser writes use /api/messages task-scope sends' \
  '\b(AddTaskDiscussionEntry|AddTaskDiscussionEntryRequest|AddTaskDiscussionEntryResponse|TaskDiscussionEntryInput|EditTaskDiscussionEntry|DeleteTaskDiscussionEntry|AddTaskDiscussionReaction|RemoveTaskDiscussionReaction|TaskDiscussionEntryMutationResponse|TaskDiscussionReactionMutationResponse|AddTaskDiscussionReactionRequest|RemoveTaskDiscussionReactionRequest|TaskDiscussionBoard|TaskDiscussionRequest|TaskDiscussionResponse|DiscussionHandler|taskServiceAddDiscussionPath|taskService(Edit|Delete)DiscussionPath|taskService(Add|Remove)DiscussionReactionPath)\b|/internal/task-service/tasks/(add-discussion|edit-discussion|delete-discussion|add-discussion-reaction|remove-discussion-reaction)' \
  internal/api/message \
  internal/api/task \
  internal/api/agent/store_adapter.go \
  internal/store/taskboard \
  --glob '*.go' \
  --glob '!*_test.go'

require_no_matches \
  'agent task-scope send must not pre-resolve through legacy task-discussion/task store helpers; SendTaskScopeMessage owns task-scope resolution and authorization' \
  'ResolveTaskForAgentTaskScopeTarget|AddTaskDiscussionEntry' \
  internal/api/agent/store_adapter.go \
  --glob '*.go'

require_no_matches \
  'canonical workspace-scoped Agent Runtime task routes must not be hidden behind the message API daemon-version gate' \
  'requireAgent(Daemon|WorkspaceDaemon|ScopeMembersDaemon)Version|minDaemonVersion' \
  internal/api/agent/api_tasks_agent.go \
  internal/api/agent/task_handler.go \
  internal/api/agent/task_create_source.go \
  --glob '*.go'

require_no_matches \
  'taskboard production code must not hand-roll task discussion message mutation/search/reaction/unread writes; use message plan and message reaction APIs' \
  'UPDATE scope_messages|INSERT INTO scope_message_search_docs|INSERT( IGNORE)? INTO scope_message_reactions|DELETE FROM scope_message_reactions|task_message_unread_count = conversation_member_states\.task_message_unread_count' \
  internal/store/taskboard internal/api/task \
  --glob '*.go' \
  --glob '!*_test.go'

require_no_matches \
	'task discussion appends must not request retired task attention/inbox side effects' \
	'TaskAttentionPlan|taskattention\.|NewInboxSyncIntent|task_inbox_active|task_inbox_sort_at' \
	internal/store/taskboard/task_scope_message.go \
	--glob '*.go' \
	--glob '!*_test.go'

require_no_matches \
  'frontend task discussion API must not expose task-native edit/delete/reaction mutation helpers' \
  '\b(editTaskDiscussion|deleteTaskDiscussion|addTaskDiscussionReaction|removeTaskDiscussionReaction)\b|/tasks/\$\{[^}]+}/discussion/\$\{[^}]+}/reactions|/tasks/\$\{[^}]+}/discussion/\$\{[^}]+}' \
  web/src/agentteam/api web/src/agentteam/workspace/tasks \
  --glob '*.ts' \
  --glob '*.tsx'

require_no_matches \
  'frontend task discussion UI must not expose task-native delete state or copy' \
  'discussion_(delete|deleted|delete_confirm)|onDeleteDiscussion|canDeleteDiscussion|deletedAt' \
  web/src/agentteam/workspace/tasks/detail web/src/agentteam/workspace/tasks/discussion web/src/types.ts web/src/agentteam/i18n/locales \
  --glob '*.ts' \
  --glob '*.tsx' \
  --glob '*.json'

require_contains \
  web/src/agentteam/workspace/tasks/detail/TaskDetailRoute.tsx \
  'editMessageInScope\(' \
  'task discussion edit UI must use the message-owned edit API instead of task-native edit routes'

require_contains \
  web/src/agentteam/workspace/tasks/detail/TaskActivityTimeline.tsx \
  'MessageReactionControls' \
  'task discussion timeline must render reactions through message-owned reusable reaction controls'

require_contains \
  web/src/agentteam/workspace/conversation/components/MessageReactionControls.tsx \
  'MessageReactionBar' \
  'message-owned reaction controls must render the ordinary message reaction bar'

require_contains \
  web/src/agentteam/workspace/conversation/components/MessageReactionControls.tsx \
  'ReactionPickerContent' \
  'message-owned reaction controls must reuse the ordinary message reaction picker'

require_no_matches \
  'frontend task discussion UI must not keep task-specific reaction chip helpers/copy; reuse ordinary message reaction UI and copy' \
  'ReactionGlyph|TaskDiscussionMessageReactions|TaskDiscussionMessageReactionChip|buildTaskDiscussionMessageReactionChips|quickTaskDiscussionMessageReactions|task_board\.discussion_react|discussion_react_with|discussion_remove_reaction|"discussion_react"\s*:' \
  web/src/agentteam/workspace/tasks/detail web/src/agentteam/workspace/tasks/discussion web/src/agentteam/i18n/locales \
  --glob '*.ts' \
  --glob '*.tsx' \
  --glob '*.json' \
  --glob '!*.test.ts' \
  --glob '!*.test.tsx'

require_no_matches \
  'task detail must not own message reaction picker/reactor glue; keep it in conversation message-owned components' \
  'function [A-Za-z0-9_]*(ReactionControls|ReactionPicker|ReactionReactors|ReactionBar)|ReactionPickerContent|MessageReactionBar|Popover|Drawer|SmilePlus|reactionIncludesActor|buildAgentLookups|maxReactionEmojiKindsPerMessage|ReactionPicker(Category|Option|Query|Section|Scroll)' \
  web/src/agentteam/workspace/tasks/detail/TaskActivityTimeline.tsx

require_no_matches \
  'production task timeline adapters must not keep old TaskDiscussion*Reaction helper names; reaction aggregation is message-backed' \
  '\b(loadTaskDiscussionReactionsForEntries|taskboardTaskDiscussionReaction(Reactor|Reactors|s)?ForDTO)\b' \
  internal/store/taskboard/discussion_page.go \
  internal/api/task/dto_conversion.go \
  --glob '*.go'

require_no_matches \
  'taskboard/app production code must not keep legacy task activity append helper names' \
  '\bappendTaskEvent\b|\bappendActivityEvent\b' \
  internal/store/taskboard internal/store/app \
  --glob '*.go' \
  --glob '!*_test.go'

require_no_matches \
  'taskboard/app production code must not insert scope_activity_events directly; use internal/store/tasktimeline.WritePlan' \
  'INSERT INTO scope_activity_events' \
  internal/store/taskboard internal/store/app \
  --glob '*.go' \
  --glob '!*_test.go'

require_file_absent \
  internal/store/app/provider_watch_task_delivery.go \
  'legacy provider-watch task delivery implementation must remain deleted; service-event task delivery owns the canonical path'

require_contains \
  internal/store/app/service_event_task_delivery.go \
  'tasktimeline\.WritePlan\(' \
  'service-event task delivery must use the unified task timeline WritePlan entrypoint'

require_no_matches \
  'service-event task delivery must not hand-roll task activity sequence allocation' \
  'latest_activity_seq = latest_activity_seq \+ 1' \
  internal/store/app/service_event_task_delivery.go

require_no_matches \
  'checkpoint timeline items must remain typed task activity, not hidden task-scope messages' \
  'task_checkpoint|MessageTypeTaskCheckpoint|appendTaskScopeCheckpointMessage|taskTimelineCheckpointMessagePredicate' \
  internal/store/taskboard internal/store/message \
  --glob '*.go' \
  --glob '!*_test.go'

if (( failures > 0 )); then
  printf '\nTask timeline write boundary check failed with %d issue(s).\n' "$failures" >&2
  exit 1
fi

printf 'Task timeline write boundary check passed.\n'
