#!/bin/sh
set -eu

if [ -n "${UNIDESK_PAC_EVALUATOR_PATH:-}" ]; then
  test -s "$UNIDESK_PAC_EVALUATOR_PATH"
else
  UNIDESK_PAC_EVALUATOR_PATH=$(mktemp)
  printf '%s' "$UNIDESK_PAC_EVALUATOR_B64" | base64 -d >"$UNIDESK_PAC_EVALUATOR_PATH"
fi
export UNIDESK_PAC_EVALUATOR_PATH
cleanup_payloads() {
  cleanup_status=$?
  trap - 0 1 2 15
  rm -f "$UNIDESK_PAC_EVALUATOR_PATH"
  if [ -n "${UNIDESK_PAC_PAYLOAD_DIR:-}" ]; then
    rm -r "$UNIDESK_PAC_PAYLOAD_DIR" 2>/dev/null || true
  fi
  exit "$cleanup_status"
}
trap cleanup_payloads 0 1 2 15
UNIDESK_PAC_HTTP_TIMEOUT_SECONDS="$UNIDESK_PAC_WAIT_TIMEOUT_SECONDS"
if [ "$UNIDESK_PAC_ACTION" = status ]; then UNIDESK_PAC_HTTP_TIMEOUT_SECONDS="$UNIDESK_PAC_READ_STEP_TIMEOUT_SECONDS"; fi

pac_read() {
  if [ "$UNIDESK_PAC_ACTION" = status ]; then timeout "$UNIDESK_PAC_READ_STEP_TIMEOUT_SECONDS" "$@"; else "$@"; fi
}

json_escape() {
  sed 's/\\/\\\\/g; s/"/\\"/g; s/	/\\t/g' | tr '\n' ' '
}

json_string() {
  printf '%s' "$1" | json_escape
}

materialize_payload() (
  payload_source_path="$1"
  payload_inline_b64="$2"
  payload_target_path="$3"
  if [ -n "$payload_source_path" ]; then
    cat "$payload_source_path" >"$payload_target_path"
  else
    printf '%s' "$payload_inline_b64" | base64 -d >"$payload_target_path"
  fi
)

payload_present() (
  payload_source_path="$1"
  payload_inline_b64="$2"
  if [ -n "$payload_source_path" ]; then
    test -s "$payload_source_path"
  else
    test -n "$payload_inline_b64"
  fi
)

now_epoch() {
  date +%s
}

duration_seconds() {
  start="$1"
  end="$2"
  if [ -n "$start" ] && [ -n "$end" ]; then
    s=$(date -d "$start" +%s 2>/dev/null || echo "")
    e=$(date -d "$end" +%s 2>/dev/null || echo "")
    if [ -n "$s" ] && [ -n "$e" ]; then
      echo $((e - s))
      return
    fi
  fi
  echo null
}

json_normalize() {
  UNIDESK_JSON_NORMALIZE_INPUT="${1:-}" node <<'NODE'
const input = String(process.env.UNIDESK_JSON_NORMALIZE_INPUT || '').trim();
if (!input) {
  process.stdout.write('{}');
  process.exit(0);
}
for (let end = input.length; end > 0; end -= 1) {
  try {
    process.stdout.write(JSON.stringify(JSON.parse(input.slice(0, end))));
    process.exit(0);
  } catch {}
}
process.stdout.write('{}');
NODE
}

api_url() {
  prepare_gitea_api_base
  printf '%s/api/v1/%s' "$UNIDESK_PAC_GITEA_API_BASE_URL" "$1"
}

prepare_gitea_api_base() {
  if [ -n "${UNIDESK_PAC_GITEA_API_BASE_URL:-}" ]; then
    return
  fi
  base="$UNIDESK_PAC_GITEA_BASE_URL"
  hostport=$(printf '%s' "$base" | sed -n 's#^http://\([^/]*\).*$#\1#p')
  if printf '%s' "$hostport" | grep -q '\.svc\.cluster\.local'; then
    host=${hostport%%:*}
    port=${hostport##*:}
    if [ "$port" = "$hostport" ]; then port=80; fi
    service=${host%%.*}
    rest=${host#*.}
    namespace=${rest%%.*}
    cluster_ip=$(pac_read kubectl -n "$namespace" get svc "$service" -o jsonpath='{.spec.clusterIP}')
    UNIDESK_PAC_GITEA_API_BASE_URL="http://$cluster_ip:$port"
  else
    UNIDESK_PAC_GITEA_API_BASE_URL="$base"
  fi
  export UNIDESK_PAC_GITEA_API_BASE_URL
}

cluster_service_url() {
  value=$1
  hostport=$(printf '%s' "$value" | sed -n 's#^https\?://\([^/]*\).*$#\1#p')
  host=${hostport%%:*}
  case "$host" in
    *.svc.cluster.local)
      service=${host%%.*}
      rest=${host#*.}
      namespace=${rest%%.*}
      cluster_ip=$(kubectl -n "$namespace" get service "$service" -o jsonpath='{.spec.clusterIP}')
      printf '%s' "$value" | sed "s#$host#$cluster_ip#"
      ;;
    *) printf '%s' "$value" ;;
  esac
}

gitea_api() {
  method="$1"
  path="$2"
  body="${3:-}"
  tmp=$(mktemp)
  if [ -n "$body" ]; then
    code=$(curl -sS --max-time "$UNIDESK_PAC_HTTP_TIMEOUT_SECONDS" -o "$tmp" -w '%{http_code}' -u "$UNIDESK_PAC_GITEA_ADMIN_USERNAME:$UNIDESK_PAC_GITEA_ADMIN_PASSWORD" \
      -H 'Content-Type: application/json' \
      -X "$method" \
      --data "$body" \
      "$(api_url "$path")" || true)
  else
    code=$(curl -sS --max-time "$UNIDESK_PAC_HTTP_TIMEOUT_SECONDS" -o "$tmp" -w '%{http_code}' -u "$UNIDESK_PAC_GITEA_ADMIN_USERNAME:$UNIDESK_PAC_GITEA_ADMIN_PASSWORD" \
      -X "$method" \
      "$(api_url "$path")" || true)
  fi
  case "$code" in
    2*) cat "$tmp"; rm -f "$tmp"; return 0 ;;
  esac
  response=$(head -c 500 "$tmp" | tr '\n' ' ' | sed 's/[[:space:]][[:space:]]*/ /g')
  rm -f "$tmp"
  printf 'gitea api %s %s failed: HTTP %s response=%s\n' "$method" "$path" "$code" "$response" >&2
  return 22
}

gitea_api_status() {
  method="$1"
  path="$2"
  status=$(curl -sS --max-time "$UNIDESK_PAC_HTTP_TIMEOUT_SECONDS" -o /dev/null -w '%{http_code}' \
    -u "$UNIDESK_PAC_GITEA_ADMIN_USERNAME:$UNIDESK_PAC_GITEA_ADMIN_PASSWORD" \
    -X "$method" \
    "$(api_url "$path")" 2>/dev/null) || status=000
  printf '%s' "${status:-000}"
}

repository_preflight() {
  repository_path="repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO"
  repository_status=$(gitea_api_status GET "$repository_path")
  case "$repository_status" in
    2*) return 0 ;;
    404)
      printf '{"ok":false,"mode":"preflight-blocked","mutation":false,"error":"gitea-repository-missing","preflight":{"repositoryExists":false,"httpStatus":"404","repository":"%s/%s","valuesPrinted":false},"valuesPrinted":false}\n' \
        "$(json_string "$UNIDESK_PAC_GITEA_OWNER")" \
        "$(json_string "$UNIDESK_PAC_GITEA_REPO")"
      return 22
      ;;
    *)
      printf '{"ok":false,"mode":"preflight-blocked","mutation":false,"error":"gitea-repository-preflight-failed","preflight":{"repositoryExists":false,"httpStatus":"%s","repository":"%s/%s","valuesPrinted":false},"valuesPrinted":false}\n' \
        "$(json_string "$repository_status")" \
        "$(json_string "$UNIDESK_PAC_GITEA_OWNER")" \
        "$(json_string "$UNIDESK_PAC_GITEA_REPO")"
      return 22
      ;;
  esac
}

ensure_token() {
  name="unidesk-pac-$(date +%Y%m%d%H%M%S)"
  body=$(printf '{"name":"%s","scopes":["all"]}' "$name")
  out=$(gitea_api POST "users/$UNIDESK_PAC_GITEA_API_USERNAME/tokens" "$body")
  printf '%s' "$out" | sed -n 's/.*"sha1"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'
}

remove_automatic_webhooks() {
  hooks=$(gitea_api GET "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks")
  hook_ids=$(HOOKS_JSON="$hooks" node <<'NODE'
const data = JSON.parse(process.env.HOOKS_JSON || '[]');
const url = process.env.UNIDESK_PAC_WEBHOOK_URL;
for (const item of data) if (item?.config?.url === url && item?.id) console.log(item.id);
NODE
)
  printf '%s\n' "$hook_ids" | while IFS= read -r hook_id; do
    [ -n "$hook_id" ] || continue
    gitea_api DELETE "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks/$hook_id" >/dev/null
  done
}

disable_automatic_webhooks_action() {
  repositories=$(UNIDESK_PAC_WEBHOOK_REPOSITORIES_JSON="$UNIDESK_PAC_WEBHOOK_REPOSITORIES_JSON" node <<'NODE'
const data = JSON.parse(process.env.UNIDESK_PAC_WEBHOOK_REPOSITORIES_JSON || '[]');
for (const item of data) console.log(`${item.id}\t${item.owner}\t${item.repo}`);
NODE
)
  repository_count=0
  removed_count=0
  tab=$(printf '\t')
  while IFS="$tab" read -r repository_id owner repo; do
    [ -n "$repository_id" ] || continue
    repository_count=$((repository_count + 1))
    UNIDESK_PAC_GITEA_OWNER=$owner
    UNIDESK_PAC_GITEA_REPO=$repo
    hooks=$(gitea_api GET "repos/$owner/$repo/hooks")
    hook_ids=$(HOOKS_JSON="$hooks" node <<'NODE'
const data = JSON.parse(process.env.HOOKS_JSON || '[]');
const url = process.env.UNIDESK_PAC_WEBHOOK_URL;
for (const item of data) if (item?.config?.url === url && item?.id) console.log(item.id);
NODE
)
    for hook_id in $hook_ids; do
      gitea_api DELETE "repos/$owner/$repo/hooks/$hook_id" >/dev/null
      removed_count=$((removed_count + 1))
    done
  done <<EOF
$repositories
EOF
  printf '{"ok":true,"mutation":true,"repositoryCount":%s,"removedWebhookCount":%s,"automaticWebhookEnabled":false,"valuesPrinted":false}\n' "$repository_count" "$removed_count"
}

manual_release_plan_json() {
  tmp=$(mktemp -d)
  trap 'rm -rf "$tmp"' EXIT INT TERM
  git clone --quiet --no-checkout "$UNIDESK_PAC_REPOSITORY_URL" "$tmp/source"
  git -C "$tmp/source" checkout --quiet --detach "$UNIDESK_PAC_MANUAL_SOURCE_COMMIT"
  source_commit=$(git -C "$tmp/source" rev-parse HEAD)
  [ "$source_commit" = "$UNIDESK_PAC_MANUAL_SOURCE_COMMIT" ]
  base_commit=${UNIDESK_PAC_MANUAL_BASE_COMMIT:-}
  base_source=explicit
  domain_plan_file="$tmp/domain-plan.json"
  if [ "$UNIDESK_PAC_MANUAL_RENDERER" = "hwlab-runtime-lane" ]; then
    if [ ! -d "$UNIDESK_PAC_MANUAL_NODE_MODULES" ]; then
      printf '%s\n' "HWLAB planner node_modules is missing at the YAML-selected workspace" >&2
      return 1
    fi
    ln -s "$UNIDESK_PAC_MANUAL_NODE_MODULES" "$tmp/source/node_modules"
    gitops_read_url=$(cluster_service_url "$UNIDESK_PAC_MANUAL_GITOPS_READ_URL")
    (
      cd "$tmp/source"
      HWLAB_CATALOG_PATH="$UNIDESK_PAC_MANUAL_ARTIFACT_CATALOG_PATH" \
      HWLAB_GITOPS_BRANCH="$UNIDESK_PAC_MANUAL_GITOPS_BRANCH" \
      HWLAB_GIT_READ_URL="$gitops_read_url" \
      HWLAB_SERVICES="$UNIDESK_PAC_MANUAL_SERVICE_IDS" \
        node scripts/ci/restore-artifact-catalog.mjs
    )
    if [ -z "$base_commit" ]; then
      base_commit=$(node - "$tmp/source/$UNIDESK_PAC_MANUAL_ARTIFACT_CATALOG_PATH.authority.json" <<'NODE'
const fs = require('node:fs');
const path = process.argv[2];
const authority = JSON.parse(fs.readFileSync(path, 'utf8'));
const commit = String(authority.catalogSourceCommitId || '');
if (!/^[0-9a-f]{40}$/u.test(commit)) {
  process.stderr.write('HWLAB release plan requires a valid catalogSourceCommitId\n');
  process.exit(1);
}
process.stdout.write(commit);
NODE
)
      base_source=catalog-source
    fi
    git -C "$tmp/source" cat-file -e "$base_commit^{commit}"
    changed_paths_file="$tmp/changed-paths.txt"
    git -C "$tmp/source" diff --name-only "$base_commit" "$source_commit" >"$changed_paths_file"
    (cd "$tmp/source" && node scripts/ci-plan.mjs \
      --lane "$UNIDESK_PAC_CONSUMER_LANE" \
      --base-ref "$base_commit" \
      --target-ref "$source_commit" \
      --artifact-catalog "$UNIDESK_PAC_MANUAL_ARTIFACT_CATALOG_PATH" \
      --services "$UNIDESK_PAC_MANUAL_SERVICE_IDS" \
      --registry-prefix "$UNIDESK_PAC_MANUAL_REGISTRY_PREFIX" \
      --base-image "$UNIDESK_PAC_MANUAL_BASE_IMAGE" \
      --release-target "$UNIDESK_PAC_MANUAL_RELEASE_TARGET" \
      --release-consumer "$UNIDESK_PAC_MANUAL_RELEASE_CONSUMER" \
      --verify-reuse-registry) >"$domain_plan_file"
  else
    if [ -z "$base_commit" ]; then
      base_commit=$(git -C "$tmp/source" rev-parse "$source_commit^")
      base_source=source-parent
    fi
    git -C "$tmp/source" cat-file -e "$base_commit^{commit}"
    changed_paths_file="$tmp/changed-paths.txt"
    git -C "$tmp/source" diff --name-only "$base_commit" "$source_commit" >"$changed_paths_file"
    printf 'null' >"$domain_plan_file"
  fi
  SOURCE_COMMIT="$source_commit" BASE_COMMIT="$base_commit" BASE_SOURCE="$base_source" node - "$changed_paths_file" "$domain_plan_file" <<'NODE'
const fs = require('node:fs');
const crypto = require('node:crypto');
const changedPaths = fs.readFileSync(process.argv[2], 'utf8').split('\n').filter(Boolean);
const domain = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
const materializationPaths = JSON.parse(process.env.UNIDESK_PAC_MANUAL_MATERIALIZATION_PATHS_JSON || '[]');
const effectiveChangedPaths = materializationPaths.length === 0
  ? changedPaths
  : changedPaths.filter((changedPath) => materializationPaths.some((inputPath) => changedPath === inputPath || changedPath.startsWith(`${inputPath}/`)));
const configuredImages = JSON.parse(process.env.UNIDESK_PAC_MANUAL_IMAGE_REPOSITORIES_JSON || '[]');
const affectedServices = domain?.affectedServices || (effectiveChangedPaths.length > 0 ? [process.env.UNIDESK_PAC_MANUAL_RUNTIME_SERVICE].filter(Boolean) : []);
const rolloutServices = domain?.rolloutServices || affectedServices;
const buildServices = domain?.buildServices || (effectiveChangedPaths.length > 0 ? configuredImages : []);
const requestedServices = domain
  ? (domain.services || [])
    .filter((service) => (service.changedPaths || []).length > 0
      || (service.envChangedPaths || []).length > 0
      || (service.codeChangedPaths || []).length > 0
      || service.runtimeConfigChanged === true)
    .map((service) => service.serviceId)
  : (effectiveChangedPaths.length > 0 ? [process.env.UNIDESK_PAC_MANUAL_RUNTIME_SERVICE].filter(Boolean) : []);
const expandedServices = affectedServices.filter((serviceId) => !requestedServices.includes(serviceId));
const envGroups = Array.isArray(domain?.envArtifactGroups) ? domain.envArtifactGroups : [];
const envReuse = domain ? {
  status: envGroups.length > 0 ? 'planned' : 'not-configured',
  groups: envGroups,
  reusedServices: domain.reusedServices || [],
  reusedServiceCount: Number(domain.serviceReusedCount || 0),
  buildSkippedServices: domain.imageBuildSkippedServices || [],
} : { status: 'not-configured', groups: [], reusedServices: [], reusedServiceCount: 0, buildSkippedServices: [] };
const stable = (value) => {
  if (Array.isArray(value)) return '[' + value.map(stable).join(',') + ']';
  if (value && typeof value === 'object') return '{' + Object.keys(value).sort().map((key) => JSON.stringify(key) + ':' + stable(value[key])).join(',') + '}';
  return JSON.stringify(value);
};
const identityScope = {
  buildServices: [...buildServices].sort(),
  rolloutServices: [...rolloutServices].sort(),
  affectedServices: [...affectedServices].sort(),
};
const genericIdentity = {
  version: 'v1',
  inputs: {
    target: process.env.UNIDESK_PAC_MANUAL_RELEASE_TARGET,
    consumer: process.env.UNIDESK_PAC_MANUAL_RELEASE_CONSUMER,
    lane: process.env.UNIDESK_PAC_CONSUMER_LANE,
    sourceCommitId: process.env.SOURCE_COMMIT,
    baseCommitId: process.env.BASE_COMMIT,
    catalog: domain?.artifactCatalog || null,
    registryProbe: domain?.planIdentity?.inputs?.registryProbe || { enabled: false, services: [] },
  },
  scope: identityScope,
};
const planIdentity = domain?.planIdentity?.value || `sha256:${crypto.createHash('sha256').update(stable(genericIdentity)).digest('hex')}`;
const plan = {
  sourceCommit: process.env.SOURCE_COMMIT,
  baseCommit: process.env.BASE_COMMIT,
  sourceBranch: process.env.UNIDESK_PAC_MANUAL_SOURCE_BRANCH,
  planIdentity,
  registryProbe: domain?.planIdentity?.inputs?.registryProbe || genericIdentity.inputs.registryProbe,
  changedPathCount: effectiveChangedPaths.length,
  changedPaths: effectiveChangedPaths,
  sourceChangedPathCount: changedPaths.length,
  materializationPaths,
  artifactCatalog: domain?.artifactCatalog || null,
  rangeBaseline: {
    mode: process.env.BASE_SOURCE,
    baseCommit: process.env.BASE_COMMIT,
    catalogSourceCommitId: domain?.artifactCatalog?.catalogSourceCommitId || null,
    catalogAligned: domain?.artifactCatalog?.catalogSourceCommitId
      ? domain.artifactCatalog.catalogSourceCommitId === process.env.BASE_COMMIT
      : null,
  },
  requestedScope: {
    affectedServiceCount: requestedServices.length,
    affectedServices: requestedServices,
  },
  envReuse,
  imageBuildCount: buildServices.length,
  buildServices,
  rolloutServiceCount: rolloutServices.length,
  rolloutServices,
  affectedServiceCount: affectedServices.length,
  affectedServices,
  scopeReview: {
    required: true,
    status: expandedServices.length > 0 ? 'expanded' : 'exact',
    expanded: expandedServices.length > 0,
    expandedServiceCount: expandedServices.length,
    expandedServices,
    instruction: expandedServices.length > 0
      ? 'fix the planner, source range, or owning YAML before trigger'
      : 'review the explicit build and rollout scope before trigger',
  },
  valuesPrinted: false,
};
process.stdout.write(JSON.stringify(plan));
NODE
}

manual_release_action() {
  plan=$(manual_release_plan_json)
  if [ "$UNIDESK_PAC_ACTION" = "manual-release-plan" ]; then
    printf '{"ok":true,"mutation":false,"plan":%s,"valuesPrinted":false}\n' "$plan"
    return
  fi
  plan_file=$(mktemp)
  printf '%s' "$plan" >"$plan_file"
  plan_gate=$(node - "$plan_file" <<'NODE'
const fs = require('node:fs');
const plan = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
if (plan.scopeReview?.expanded === true) process.stdout.write('expanded');
else if (!process.env.UNIDESK_PAC_MANUAL_PLAN_IDENTITY || process.env.UNIDESK_PAC_MANUAL_PLAN_IDENTITY !== plan.planIdentity) process.stdout.write('identity-changed');
else if (Number(plan.imageBuildCount || 0) === 0 && Number(plan.rolloutServiceCount || 0) === 0 && Number(plan.affectedServiceCount || 0) === 0) process.stdout.write('empty');
else process.stdout.write('ready');
NODE
)
  if [ "$plan_gate" = "expanded" ]; then
    rm -f "$plan_file"
    printf '{"ok":false,"mutation":false,"error":"release-plan-scope-expanded","plan":%s,"webhook":null,"valuesPrinted":false}\n' "$plan"
    return 1
  fi
  if [ "$plan_gate" = "identity-changed" ]; then
    rm -f "$plan_file"
    printf '{"ok":false,"mutation":false,"error":"release-plan-identity-changed","plan":%s,"webhook":null,"valuesPrinted":false}\n' "$plan"
    return 1
  fi
  if [ "$plan_gate" = "empty" ]; then
    rm -f "$plan_file"
    printf '{"ok":false,"mutation":false,"error":"release-plan-empty-scope","plan":%s,"webhook":null,"valuesPrinted":false}\n' "$plan"
    return 1
  fi
  payload_file=$(mktemp)
  node - "$plan_file" >"$payload_file" <<'NODE'
const fs = require('node:fs');
const plan = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
const byKind = { added: [], removed: [], modified: plan.changedPaths };
const owner = process.env.UNIDESK_PAC_GITEA_OWNER;
const repo = process.env.UNIDESK_PAC_GITEA_REPO;
const url = process.env.UNIDESK_PAC_REPOSITORY_URL.replace(/\/+$/u, '');
const reviewedPlan = {
  version: 'v1',
  planIdentity: plan.planIdentity,
  target: process.env.UNIDESK_PAC_MANUAL_RELEASE_TARGET,
  consumer: process.env.UNIDESK_PAC_MANUAL_RELEASE_CONSUMER,
  sourceCommit: plan.sourceCommit,
  baseCommit: plan.baseCommit,
  buildServices: plan.buildServices,
  rolloutServices: plan.rolloutServices,
  affectedServices: plan.affectedServices,
};
const reviewedPlanEnvelope = `unidesk-release-plan-v1.${Buffer.from(JSON.stringify(reviewedPlan)).toString('base64url')}`;
const commit = { id: plan.sourceCommit, message: reviewedPlanEnvelope, url: `${url}/commit/${plan.sourceCommit}`, added: byKind.added, removed: byKind.removed, modified: byKind.modified, author: { name: 'unidesk-cli', email: '', username: 'unidesk-cli' }, committer: { name: 'unidesk-cli', email: '', username: 'unidesk-cli' } };
const user = { id: 0, login: 'unidesk-cli', full_name: 'UniDesk CLI', email: '', avatar_url: '', username: 'unidesk-cli' };
process.stdout.write(JSON.stringify({ ref: `refs/heads/${plan.sourceBranch}`, before: plan.baseCommit, after: plan.sourceCommit, compare_url: `${url}/compare/${plan.baseCommit}...${plan.sourceCommit}`, commits: [commit], total_commits: 1, head_commit: commit, repository: { id: 0, owner: { ...user, login: owner, username: owner }, name: repo, full_name: `${owner}/${repo}`, private: true, html_url: url, clone_url: `${url}.git`, default_branch: plan.sourceBranch }, pusher: user, sender: user }));
NODE
  signature=$(node - "$payload_file" <<'NODE'
const fs = require('node:fs');
const crypto = require('node:crypto');
const payload = fs.readFileSync(process.argv[2]);
process.stdout.write(crypto.createHmac('sha256', process.env.UNIDESK_PAC_WEBHOOK_SECRET).update(payload).digest('hex'));
NODE
)
  service_ip=$(kubectl -n "$UNIDESK_PAC_RELEASE_NAMESPACE" get service "$UNIDESK_PAC_CONTROLLER_SERVICE_NAME" -o jsonpath='{.spec.clusterIP}')
  response_tmp=$(mktemp)
  delivery="unidesk-manual-$(date +%s)-$$"
  http_code=$(curl -sS -o "$response_tmp" -w '%{http_code}' -X POST "http://$service_ip:$UNIDESK_PAC_CONTROLLER_SERVICE_PORT" \
    -H 'Content-Type: application/json' \
    -H 'X-Gitea-Event-Type: push' \
    -H "X-Gitea-Delivery: $delivery" \
    -H "X-Gitea-Signature: $signature" \
    --data-binary "@$payload_file")
  case "$http_code" in 2*) ok=true ;; *) ok=false ;; esac
  rm -f "$response_tmp"
  rm -f "$plan_file" "$payload_file"
  printf '{"ok":%s,"mutation":true,"plan":%s,"webhook":{"event":"push","delivery":"%s","httpStatus":%s,"sourceCommit":"%s","valuesPrinted":false},"valuesPrinted":false}\n' "$ok" "$plan" "$(json_string "$delivery")" "$http_code" "$(json_string "$UNIDESK_PAC_MANUAL_SOURCE_COMMIT")"
  [ "$ok" = true ]
}

repository_manifest() {
  cat <<EOF
apiVersion: pipelinesascode.tekton.dev/v1alpha1
kind: Repository
metadata:
  name: $UNIDESK_PAC_REPOSITORY_NAME
  namespace: $UNIDESK_PAC_TARGET_NAMESPACE
  labels:
    app.kubernetes.io/managed-by: unidesk
    app.kubernetes.io/part-of: $UNIDESK_PAC_PART_OF
    unidesk.ai/spec: $UNIDESK_PAC_SPEC
spec:
  url: $UNIDESK_PAC_REPOSITORY_URL
  git_provider:
    type: gitea
    url: $UNIDESK_PAC_GITEA_BASE_URL
    secret:
      name: $UNIDESK_PAC_SECRET_NAME
      key: $UNIDESK_PAC_TOKEN_KEY
    webhook_secret:
      name: $UNIDESK_PAC_SECRET_NAME
      key: $UNIDESK_PAC_WEBHOOK_SECRET_KEY
  concurrency_limit: $UNIDESK_PAC_CONCURRENCY_LIMIT
  params:
$(printf '%s' "$UNIDESK_PAC_REPOSITORY_PARAMS_JSON" | node -e 'const fs=require("fs"); const p=JSON.parse(fs.readFileSync(0,"utf8")); for (const [name,value] of Object.entries(p)) console.log(`    - name: ${name}\n      value: ${JSON.stringify(String(value))}`);')
EOF
}

consumer_rbac_manifest() {
  pipeline_verbs='["get", "list", "watch", "create", "patch", "update"]'
  cat <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: unidesk-pac-consumer-runner
  namespace: $UNIDESK_PAC_TARGET_NAMESPACE
  labels:
    app.kubernetes.io/managed-by: unidesk
    app.kubernetes.io/part-of: $UNIDESK_PAC_PART_OF
    unidesk.ai/spec: $UNIDESK_PAC_SPEC
rules:
  - apiGroups: ["tekton.dev"]
    resources: ["pipelineruns", "taskruns"]
    verbs: $pipeline_verbs
  - apiGroups: [""]
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: unidesk-pac-consumer-runner-default
  namespace: $UNIDESK_PAC_TARGET_NAMESPACE
  labels:
    app.kubernetes.io/managed-by: unidesk
    app.kubernetes.io/part-of: $UNIDESK_PAC_PART_OF
    unidesk.ai/spec: $UNIDESK_PAC_SPEC
subjects:
  - kind: ServiceAccount
    name: default
    namespace: $UNIDESK_PAC_TARGET_NAMESPACE
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: unidesk-pac-consumer-runner
EOF
}

pac_repository_secret_manifest() {
  node -e '
const fs = require("node:fs");
const input = fs.readFileSync(0);
const split = input.indexOf(0);
if (split < 0) process.exit(2);
const token = input.subarray(0, split);
const webhook = input.subarray(split + 1);
const data = {};
data[process.env.UNIDESK_PAC_TOKEN_KEY] = token.toString("base64");
data[process.env.UNIDESK_PAC_WEBHOOK_SECRET_KEY] = webhook.toString("base64");
process.stdout.write(JSON.stringify({
  apiVersion: "v1",
  kind: "Secret",
  metadata: { name: process.env.UNIDESK_PAC_SECRET_NAME, namespace: process.env.UNIDESK_PAC_TARGET_NAMESPACE },
  type: "Opaque",
  data,
}));
'
}

argo_repository_secret_manifest() {
  node -e '
const fs = require("node:fs");
const token = fs.readFileSync(0);
const encoded = (value) => Buffer.from(value, "utf8").toString("base64");
process.stdout.write(JSON.stringify({
  apiVersion: "v1",
  kind: "Secret",
  metadata: {
    name: process.env.UNIDESK_PAC_ARGO_REPOSITORY_SECRET_NAME,
    namespace: process.env.UNIDESK_PAC_ARGO_NAMESPACE,
    labels: { "argocd.argoproj.io/secret-type": "repository" },
  },
  type: "Opaque",
  data: {
    url: encoded(process.env.UNIDESK_PAC_ARGO_REPOSITORY_URL || ""),
    username: encoded(process.env.UNIDESK_PAC_ARGO_REPOSITORY_USERNAME || ""),
    password: token.toString("base64"),
  },
}));
'
}

apply_release() {
  tmp=$(mktemp)
  materialize_payload "${UNIDESK_PAC_RELEASE_MANIFEST_PATH:-}" "${UNIDESK_PAC_RELEASE_MANIFEST_B64:-}" "$tmp"
  kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER" -f "$tmp" >/dev/null
  rm -f "$tmp"
}

apply_admission_manifest() {
  tmp=$(mktemp)
  materialize_payload "${UNIDESK_PAC_ADMISSION_MANIFEST_PATH:-}" "${UNIDESK_PAC_ADMISSION_MANIFEST_B64:-}" "$tmp"
  kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER-admission-bootstrap" -f "$tmp" >/dev/null
  rm -f "$tmp"
}

wait_ready() {
  timeout="${UNIDESK_PAC_WAIT_TIMEOUT_SECONDS:-55}"
  end=$(( $(now_epoch) + timeout ))
  while [ "$(now_epoch)" -lt "$end" ]; do
    ready=$(kubectl -n "$UNIDESK_PAC_RELEASE_NAMESPACE" get deploy "$UNIDESK_PAC_CONTROLLER_SERVICE_NAME" -o jsonpath='{.status.readyReplicas}' 2>/dev/null || true)
    desired=$(kubectl -n "$UNIDESK_PAC_RELEASE_NAMESPACE" get deploy "$UNIDESK_PAC_CONTROLLER_SERVICE_NAME" -o jsonpath='{.spec.replicas}' 2>/dev/null || true)
    if [ "${ready:-0}" = "${desired:-1}" ] && [ "${ready:-0}" != "0" ]; then
      return 0
    fi
    sleep 2
  done
  return 1
}

apply_action() {
  preflight_tmp=$(mktemp)
  if ! repository_preflight >"$preflight_tmp"; then
    cat "$preflight_tmp"
    rm -f "$preflight_tmp"
    return 22
  fi
  rm -f "$preflight_tmp"
  if [ "$UNIDESK_PAC_DRY_RUN" = "1" ]; then
    printf '{"ok":true,"mode":"dry-run","mutation":false,"preflight":{"repositoryExists":true,"httpStatus":"2xx","repository":"%s/%s","valuesPrinted":false},"valuesPrinted":false}\n' \
      "$(json_string "$UNIDESK_PAC_GITEA_OWNER")" \
      "$(json_string "$UNIDESK_PAC_GITEA_REPO")"
    return
  fi
  apply_release
  admission_applied=false
  if [ "${UNIDESK_PAC_DELIVERY_PROVENANCE_REQUIRED:-0}" = "1" ]; then
    apply_admission_manifest
    admission_applied=true
  fi
  kubectl create ns "$UNIDESK_PAC_TARGET_NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
  if [ "${UNIDESK_PAC_DELIVERY_PROVENANCE_REQUIRED:-0}" = "1" ]; then
    rbac_tmp=$(mktemp)
    materialize_payload "${UNIDESK_PAC_CONSUMER_RBAC_MANIFEST_PATH:-}" "${UNIDESK_PAC_CONSUMER_RBAC_MANIFEST_B64:-}" "$rbac_tmp"
    kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER-compatibility-bootstrap" -f "$rbac_tmp" >/dev/null
    rm -f "$rbac_tmp"
  else
    consumer_rbac_manifest | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER" -f - >/dev/null
  fi
  if payload_present "${UNIDESK_PAC_RUNNER_SERVICE_ACCOUNT_MANIFEST_PATH:-}" "${UNIDESK_PAC_RUNNER_SERVICE_ACCOUNT_MANIFEST_B64:-}"; then
    runner_tmp=$(mktemp)
    materialize_payload "${UNIDESK_PAC_RUNNER_SERVICE_ACCOUNT_MANIFEST_PATH:-}" "${UNIDESK_PAC_RUNNER_SERVICE_ACCOUNT_MANIFEST_B64:-}" "$runner_tmp"
    kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER-runner-bootstrap" -f "$runner_tmp" >/dev/null
    rm -f "$runner_tmp"
  fi
  token=$(ensure_token)
  test -n "$token"
  { printf '%s' "$token"; printf '\0'; printf '%s' "$UNIDESK_PAC_WEBHOOK_SECRET"; } \
    | pac_repository_secret_manifest \
    | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER-repository-secret" -f - >/dev/null
  if [ "${UNIDESK_PAC_REPOSITORY_APPLY:-1}" = "1" ]; then
    repository_manifest | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER" -f - >/dev/null
  else
    kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get repository.pipelinesascode.tekton.dev "$UNIDESK_PAC_REPOSITORY_NAME" >/dev/null
  fi
  argo_bootstrap=false
  if payload_present "${UNIDESK_PAC_ARGO_BOOTSTRAP_MANIFEST_PATH:-}" "${UNIDESK_PAC_ARGO_BOOTSTRAP_MANIFEST_B64:-}"; then
    if [ -n "${UNIDESK_PAC_ARGO_REPOSITORY_SECRET_NAME:-}" ]; then
      printf '%s' "$token" \
        | argo_repository_secret_manifest \
        | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER-argocd-repository" -f - >/dev/null
    fi
    argo_tmp=$(mktemp)
    materialize_payload "${UNIDESK_PAC_ARGO_BOOTSTRAP_MANIFEST_PATH:-}" "${UNIDESK_PAC_ARGO_BOOTSTRAP_MANIFEST_B64:-}" "$argo_tmp"
    kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER" -f "$argo_tmp" >/dev/null
    rm -f "$argo_tmp"
    kubectl -n "$UNIDESK_PAC_ARGO_NAMESPACE" annotate application "$UNIDESK_PAC_ARGO_APPLICATION" argocd.argoproj.io/refresh=hard --overwrite >/dev/null
    argo_bootstrap=true
  fi
  remove_automatic_webhooks
  hook_id=""
  ready=false
  if wait_ready; then ready=true; fi
  prepare_admission_provenance_state
  admission_ready=true
  if [ "${UNIDESK_PAC_DELIVERY_PROVENANCE_REQUIRED:-0}" = "1" ]; then
    admission_ready=$(printf '%s' "$UNIDESK_PAC_ADMISSION_STATE_JSON" | node -e 'let input="";process.stdin.on("data",chunk=>input+=chunk);process.stdin.on("end",()=>process.stdout.write(JSON.parse(input).ready===true?"true":"false"));')
  fi
  ok=false
  if [ "$ready" = true ]; then ok=true; fi
  webhook_present=false
  printf '{"ok":%s,"mode":"confirmed","mutation":true,"controllerReady":%s,"admission":{"applied":%s,"ready":%s},"admissionProvenance":%s,"argoBootstrap":%s,"preflight":{"repositoryExists":true,"httpStatus":"2xx","repository":"%s/%s","valuesPrinted":false},"webhook":{"present":%s,"id":"%s","url":"%s"},"repository":"%s","namespace":"%s","valuesPrinted":false}\n' "$ok" "$ready" "$admission_applied" "$admission_ready" "$UNIDESK_PAC_ADMISSION_STATE_JSON" "$argo_bootstrap" "$(json_string "$UNIDESK_PAC_GITEA_OWNER")" "$(json_string "$UNIDESK_PAC_GITEA_REPO")" "$webhook_present" "$(json_string "$hook_id")" "$(json_string "$UNIDESK_PAC_WEBHOOK_URL")" "$(json_string "$UNIDESK_PAC_REPOSITORY_NAME")" "$(json_string "$UNIDESK_PAC_TARGET_NAMESPACE")"
}

condition_status() {
  pac_read kubectl -n "$1" get "$2" "$3" -o jsonpath='{range .status.conditions[*]}{.type}={.status}:{.reason}{";"}{end}' 2>/dev/null || true
}

admission_provenance_state() {
  if [ "${UNIDESK_PAC_DELIVERY_PROVENANCE_REQUIRED:-0}" != "1" ]; then
    printf '{"configured":false,"required":false,"ready":null,"reasons":["consumer-admission-provenance-not-required"],"valuesPrinted":false}'
    return
  fi
  policy_file=$(mktemp)
  binding_file=$(mktemp)
  role_file=$(mktemp)
  role_binding_file=$(mktemp)
  pac_read kubectl get validatingadmissionpolicy "$UNIDESK_PAC_ADMISSION_POLICY_NAME" -o json >"$policy_file" 2>/dev/null || printf '{}' >"$policy_file"
  pac_read kubectl get validatingadmissionpolicybinding "$UNIDESK_PAC_ADMISSION_BINDING_NAME" -o json >"$binding_file" 2>/dev/null || printf '{}' >"$binding_file"
  pac_read kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get role unidesk-pac-consumer-runner -o json >"$role_file" 2>/dev/null || printf '{}' >"$role_file"
  pac_read kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get rolebinding unidesk-pac-consumer-runner-default -o json >"$role_binding_file" 2>/dev/null || printf '{}' >"$role_binding_file"
  default_rules=$(pac_read kubectl auth can-i --list --as="system:serviceaccount:$UNIDESK_PAC_TARGET_NAMESPACE:default" -n "$UNIDESK_PAC_TARGET_NAMESPACE" 2>/dev/null || true)
  default_can_mutate=unknown
  if [ -n "$default_rules" ]; then
    default_can_mutate=false
    if printf '%s\n' "$default_rules" | awk '$1 == "pipelineruns.tekton.dev" || $1 == "taskruns.tekton.dev" { if ($0 ~ /(create|patch|update|delete)/) found=1 } END { exit found ? 0 : 1 }'; then default_can_mutate=true; fi
  fi
  export UNIDESK_PAC_DEFAULT_CAN_MUTATE="$default_can_mutate"
  node - "$policy_file" "$binding_file" "$role_file" "$role_binding_file" <<'NODE'
const fs = require('node:fs');
const { evaluatePacAdmissionState } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
function read(path) {
  try { return JSON.parse(fs.readFileSync(path, 'utf8') || '{}'); } catch { return {}; }
}
process.stdout.write(JSON.stringify(evaluatePacAdmissionState({
  policy: read(process.argv[2]),
  binding: read(process.argv[3]),
  role: read(process.argv[4]),
  roleBinding: read(process.argv[5]),
  namespace: process.env.UNIDESK_PAC_TARGET_NAMESPACE || null,
  defaultCanMutate: process.env.UNIDESK_PAC_DEFAULT_CAN_MUTATE === 'unknown' ? null : process.env.UNIDESK_PAC_DEFAULT_CAN_MUTATE === 'true',
  expected: {
    targetId: process.env.UNIDESK_PAC_TARGET_ID || '',
    policyName: process.env.UNIDESK_PAC_ADMISSION_POLICY_NAME || '',
    bindingName: process.env.UNIDESK_PAC_ADMISSION_BINDING_NAME || '',
    version: process.env.UNIDESK_PAC_ADMISSION_VERSION || '',
    controllerIdentity: process.env.UNIDESK_PAC_ADMISSION_CONTROLLER_IDENTITY || '',
    configSha256: process.env.UNIDESK_PAC_ADMISSION_CONFIG_SHA256 || '',
    rbacConfigSha256: process.env.UNIDESK_PAC_CONSUMER_RBAC_CONFIG_SHA256 || '',
  },
})));
NODE
  rm -f "$policy_file" "$binding_file" "$role_file" "$role_binding_file"
}

prepare_admission_provenance_state() {
  UNIDESK_PAC_ADMISSION_STATE_JSON=$(admission_provenance_state)
  export UNIDESK_PAC_ADMISSION_STATE_JSON
}

pipeline_rows() {
  payload_file=$(mktemp)
  if ! pac_read kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get pipelinerun -l "pipelinesascode.tekton.dev/repository=$UNIDESK_PAC_REPOSITORY_NAME" -o json >"$payload_file" 2>/dev/null; then
    rm -f "$payload_file"
    return 1
  fi
  node - "$payload_file" <<'NODE'
const fs = require('node:fs');
const { classifyPacPipelineRun, pipelineRunMatchesConsumer } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
const input = fs.readFileSync(process.argv[2], 'utf8') || '{"items":[]}';
const data = input ? JSON.parse(input) : { items: [] };
function firstString(...values) {
  for (const value of values) {
    if (typeof value === 'string' && value.length > 0) return value;
  }
  return null;
}
function cond(item) {
  const c = (item.status?.conditions || []).find((x) => x.type === 'Succeeded') || {};
  return { status: c.status || '', reason: c.reason || '' };
}
function mapParams(params) {
  const out = {};
  for (const item of params || []) {
    if (!item || typeof item.name !== 'string') continue;
    if (typeof item.value === 'string') out[item.name] = item.value;
    else if (item.value !== undefined && item.value !== null) out[item.name] = JSON.stringify(item.value);
  }
  return out;
}
function extractCommit(item, params) {
  const labels = item.metadata?.labels || {};
  const annotations = item.metadata?.annotations || {};
  return firstString(
    labels['pipelinesascode.tekton.dev/sha'],
    labels['pipelinesascode.tekton.dev/commit'],
    labels['agentrun.pikastech.local/source-commit'],
    labels['unidesk.ai/source-commit'],
    labels['hwlab.pikastech.local/source-commit'],
    annotations['pipelinesascode.tekton.dev/sha'],
    annotations['pipelinesascode.tekton.dev/commit'],
    annotations['unidesk.ai/source-commit'],
    params.revision,
    params.source_revision,
    params.sourceCommit,
    params.source_commit,
    params.git_sha,
    params.sha,
    params.commit
  );
}
function durationSeconds(item) {
  const start = item.status?.startTime;
  const done = item.status?.completionTime;
  if (!start) return null;
  return Math.max(0, Math.round(((done ? Date.parse(done) : Date.now()) - Date.parse(start)) / 1000));
}
const consumer = JSON.parse(Buffer.from(process.env.UNIDESK_PAC_CONSUMER_CONFIG_B64 || 'e30=', 'base64').toString('utf8'));
consumer.admissionState = JSON.parse(process.env.UNIDESK_PAC_ADMISSION_STATE_JSON || '{}');
const rows = (data.items || [])
  .filter((item) => pipelineRunMatchesConsumer(consumer, item))
  .map((item) => ({ item, classification: classifyPacPipelineRun(consumer, item) }))
  .filter(({ classification }) => classification.deliveryClass === 'outer-pac-event' && classification.deliveryAuthorityEligible === true)
  .sort((a, b) => Date.parse(b.item.metadata?.creationTimestamp || 0) - Date.parse(a.item.metadata?.creationTimestamp || 0))
  .slice(0, 8)
  .map(({ item, classification }) => {
    const c = cond(item);
    const params = mapParams(item.spec?.params);
    return {
      name: item.metadata.name,
      status: c.status,
      reason: c.reason,
      createdAt: item.metadata.creationTimestamp || null,
      startTime: item.status?.startTime || null,
      completionTime: item.status?.completionTime || null,
      durationSeconds: durationSeconds(item),
      sourceCommit: extractCommit(item, params),
      deliveryClass: classification.deliveryClass,
      deliveryAuthorityEligible: classification.deliveryAuthorityEligible,
      classification,
    };
  });
process.stdout.write(JSON.stringify(rows));
NODE
  rm -f "$payload_file"
}

history_rows() {
  node <<'NODE'
const fs = require('node:fs');
const cp = require('node:child_process');
const { classifyPacPipelineRun, evaluatePacAdmissionState, extractPacArtifactEvidence, parsePacLogRecords, pipelineRunMatchesConsumer, taskTerminalRecord } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
const limit = Math.max(1, Math.min(50, Number.parseInt(process.env.UNIDESK_PAC_HISTORY_LIMIT || '5', 10) || 5));
const detailId = process.env.UNIDESK_PAC_HISTORY_ID || '';
const consumers = JSON.parse(Buffer.from(process.env.UNIDESK_PAC_HISTORY_CONSUMERS_B64 || 'W10=', 'base64').toString('utf8'));
const displayTimeZone = process.env.UNIDESK_PAC_DISPLAY_TIME_ZONE || 'UTC';
const kubectlJsonErrors = [];
const displayTimeFormatter = new Intl.DateTimeFormat('sv-SE', {
  timeZone: displayTimeZone,
  year: 'numeric',
  month: '2-digit',
  day: '2-digit',
  hour: '2-digit',
  minute: '2-digit',
  second: '2-digit',
  hour12: false,
});

function kubectlJson(args, fallback, context) {
  const tmpDir = fs.mkdtempSync('/tmp/unidesk-pac-history-');
  const outPath = `${tmpDir}/kubectl.json`;
  let outFd = null;
  try {
    outFd = fs.openSync(outPath, 'w');
    const result = cp.spawnSync('kubectl', args, {
      stdio: ['ignore', outFd, 'pipe'],
      encoding: 'utf8',
      timeout: 30000,
      maxBuffer: 1024 * 1024,
    });
    fs.closeSync(outFd);
    outFd = null;
    if (result.error || result.status !== 0) {
      kubectlJsonErrors.push({
        context: context || args.join(' '),
        status: result.status,
        error: result.error ? String(result.error.message || result.error) : null,
        stderr: String(result.stderr || '').slice(0, 1000),
      });
      return fallback;
    }
    const out = fs.readFileSync(outPath, 'utf8');
    return JSON.parse(out || JSON.stringify(fallback));
  } catch (error) {
    kubectlJsonErrors.push({
      context: context || args.join(' '),
      status: null,
      error: String(error && error.message ? error.message : error).slice(0, 1000),
      stderr: null,
    });
    return fallback;
  } finally {
    if (outFd !== null) {
      try { fs.closeSync(outFd); } catch {}
    }
    try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
  }
}

let admissionPolicy = null;
let admissionBinding = null;
const admissionRbac = new Map();

function defaultCanMutate(namespace) {
  const waitTimeoutSeconds = Number.parseInt(process.env.UNIDESK_PAC_WAIT_TIMEOUT_SECONDS || '', 10);
  if (!Number.isInteger(waitTimeoutSeconds) || waitTimeoutSeconds <= 0) {
    kubectlJsonErrors.push({ context: `${namespace}:default-can-mutation`, status: null, error: 'invalid-yaml-wait-timeout', stderr: '' });
    return null;
  }
  const result = cp.spawnSync('kubectl', ['auth', 'can-i', '--list', `--as=system:serviceaccount:${namespace}:default`, '-n', namespace], {
    encoding: 'utf8',
    timeout: waitTimeoutSeconds * 1000,
  });
  if (result.error || result.status !== 0) {
    kubectlJsonErrors.push({ context: `${namespace}:default-can-mutation`, status: result.status, error: result.error ? String(result.error.message || result.error) : null, stderr: String(result.stderr || '').slice(0, 1000) });
    return null;
  }
  const resources = new Set(['pipelineruns.tekton.dev', 'taskruns.tekton.dev']);
  const mutatingVerbs = new Set(['create', 'patch', 'update', 'delete', 'deletecollection']);
  return String(result.stdout || '').split(/\r?\n/u).some((line) => {
    const columns = line.trim().split(/\s+/u).map((column) => column.replace(/^\[|\]$/gu, ''));
    const resourceMatches = resources.has(columns[0]) || columns[0] === '*' || columns[0] === '*.*';
    return resourceMatches && columns.some((column) => mutatingVerbs.has(column) || column === '*');
  });
}

function admissionStateForConsumer(consumer) {
  const expected = consumer.deliveryProvenance;
  if (!expected || expected.required !== true) return { configured: false, required: false, ready: null, blocking: false, warning: false, warnings: [], reasons: ['consumer-admission-provenance-not-required'], valuesPrinted: false };
  admissionPolicy ||= kubectlJson(['get', 'validatingadmissionpolicy', expected.policyName, '-o', 'json'], {}, 'admission-policy');
  admissionBinding ||= kubectlJson(['get', 'validatingadmissionpolicybinding', expected.bindingName, '-o', 'json'], {}, 'admission-binding');
  if (!admissionRbac.has(consumer.namespace)) {
    admissionRbac.set(consumer.namespace, {
      role: kubectlJson(['-n', consumer.namespace, 'get', 'role', 'unidesk-pac-consumer-runner', '-o', 'json'], {}, `${consumer.id}:default-role`),
      roleBinding: kubectlJson(['-n', consumer.namespace, 'get', 'rolebinding', 'unidesk-pac-consumer-runner-default', '-o', 'json'], {}, `${consumer.id}:default-rolebinding`),
      defaultCanMutate: defaultCanMutate(consumer.namespace),
    });
  }
  const rbac = admissionRbac.get(consumer.namespace);
  return evaluatePacAdmissionState({
    policy: admissionPolicy,
    binding: admissionBinding,
    role: rbac.role,
    roleBinding: rbac.roleBinding,
    namespace: consumer.namespace,
    defaultCanMutate: rbac.defaultCanMutate,
    expected: {
      targetId: expected.targetId,
      policyName: expected.policyName,
      bindingName: expected.bindingName,
      version: expected.version,
      controllerIdentity: expected.controllerIdentity,
      configSha256: expected.configSha256,
      rbacConfigSha256: expected.rbacConfigSha256,
    },
  });
}

function condition(item) {
  const c = (item.status?.conditions || []).find((x) => x.type === 'Succeeded') || {};
  return { status: c.status || '', reason: c.reason || '', message: c.message || '' };
}

function durationSeconds(start, done) {
  if (!start) return null;
  const startMs = Date.parse(start);
  const doneMs = done ? Date.parse(done) : Date.now();
  if (!Number.isFinite(startMs) || !Number.isFinite(doneMs)) return null;
  return Math.max(0, Math.round((doneMs - startMs) / 1000));
}

function formatDisplayTime(value) {
  if (!value) return null;
  const date = new Date(value);
  if (!Number.isFinite(date.getTime())) return null;
  return displayTimeFormatter.format(date);
}

function mapParams(params) {
  const out = {};
  for (const item of params || []) {
    if (!item || typeof item.name !== 'string') continue;
    if (typeof item.value === 'string') out[item.name] = item.value;
    else if (item.value !== undefined && item.value !== null) out[item.name] = JSON.stringify(item.value);
  }
  return out;
}

function firstString(...values) {
  for (const value of values) {
    if (typeof value === 'string' && value.length > 0) return value;
  }
  return null;
}

function extractCommit(item, params) {
  const labels = item.metadata?.labels || {};
  const annotations = item.metadata?.annotations || {};
  return firstString(
    labels['pipelinesascode.tekton.dev/sha'],
    labels['pipelinesascode.tekton.dev/commit'],
    labels['unidesk.ai/source-commit'],
    labels['hwlab.pikastech.local/source-commit'],
    labels['agentrun.pikastech.local/source-commit'],
    annotations['pipelinesascode.tekton.dev/sha'],
    annotations['pipelinesascode.tekton.dev/commit'],
    annotations['unidesk.ai/source-commit'],
    params.revision,
    params.source_revision,
    params.sourceCommit,
    params.source_commit,
    params.git_sha,
    params.sha,
    params.commit
  );
}

function extractBranch(item, params) {
  const labels = item.metadata?.labels || {};
  const annotations = item.metadata?.annotations || {};
  return firstString(
    labels['pipelinesascode.tekton.dev/branch'],
    annotations['pipelinesascode.tekton.dev/branch'],
    params.source_branch,
    params.branch,
    params.git_branch
  );
}

function recursiveEnvReuse(value, state) {
  if (value === null || value === undefined) return;
  if (Array.isArray(value)) {
    for (const item of value) recursiveEnvReuse(item, state);
    return;
  }
  if (typeof value !== 'object') return;
  if (typeof value.buildSkippedCount === 'number') state.buildSkippedCount = value.buildSkippedCount;
  if (typeof value.serviceReusedCount === 'number') state.serviceReusedCount = value.serviceReusedCount;
  if (typeof value.reusedServiceCount === 'number') state.serviceReusedCount = value.reusedServiceCount;
  if (typeof value.envReuse === 'string') state.status = value.envReuse;
  if (typeof value.envReuseStatus === 'string') state.status = value.envReuseStatus;
  if (typeof value.envIdentity === 'string') state.identity = value.envIdentity;
  if (value.stageTimings && typeof value.stageTimings === 'object' && !Array.isArray(value.stageTimings)) {
    for (const [key, timing] of Object.entries(value.stageTimings)) {
      if (typeof timing === 'number' && Number.isFinite(timing)) state.stageTimings[key] = timing;
    }
  }
  if (value.envReuse && typeof value.envReuse === 'object') {
    const env = value.envReuse;
    if (typeof env.mode === 'string') state.status = env.mode;
    if (typeof env.status === 'string') state.status = env.status;
    if (typeof env.dependencyReuse === 'string') state.status = env.dependencyReuse;
    if (typeof env.buildSkippedCount === 'number') state.buildSkippedCount = env.buildSkippedCount;
    if (typeof env.serviceReusedCount === 'number') state.serviceReusedCount = env.serviceReusedCount;
  }
  if (typeof value.status === 'string' && /reus|skip|built|publish/u.test(value.status)) state.status = value.status;
  if (state.traceId === null && typeof value.traceId === 'string' && /^[0-9a-f]{32}$/u.test(value.traceId)) state.traceId = value.traceId;
  if (state.firstBreak === null && value.firstBreak && typeof value.firstBreak === 'object') state.firstBreak = boundedFailureRecord(value.firstBreak);
  if (state.error === null && value.error && typeof value.error === 'object') state.error = boundedFailureRecord(value.error);
  for (const item of Object.values(value)) recursiveEnvReuse(item, state);
}

function boundedFailureRecord(value) {
  const input = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
  const token = (item) => typeof item === 'string' && /^[A-Za-z0-9_.-]{1,100}$/u.test(item) ? item : null;
  const summary = (item) => typeof item === 'string'
    ? item.replace(/(authorization\s*[:=]\s*)(?:bearer\s+)?\S+/giu, '$1[REDACTED]')
      .replace(/(token|password|secret)(\s*[:=]\s*)\S+/giu, '$1$2[REDACTED]')
      .replace(/https?:\/\/[^\s/]+@/giu, 'https://[REDACTED]@')
      .replace(/([?&][^=\s]+)=([^&\s]+)/gu, '$1=[REDACTED]')
      .replace(/\s+/gu, ' ').trim().slice(0, 480)
    : null;
  return {
    type: token(input.type),
    code: token(input.code),
    phase: token(input.phase),
    httpStatus: Number.isInteger(input.httpStatus) ? input.httpStatus : null,
    exitCode: Number.isInteger(input.exitCode) ? input.exitCode : null,
    reason: summary(input.reason),
    message: summary(input.message),
    stderrSummary: summary(input.stderrSummary),
    valuesPrinted: false,
  };
}

function envReuseForPipelineRun(namespace, name, taskRuns) {
  const collectLogs = detailId.length > 0 && detailId === name;
  const state = { status: null, identity: null, buildSkippedCount: null, serviceReusedCount: null, buildCache: null, stageTimings: {}, source: collectLogs ? 'pipeline-task-logs' : 'taskrun-status-only', sourceObservation: null, artifact: null, collector: null, traceId: null, firstBreak: null, error: null };
  const matching = (taskRuns.items || []).filter((item) => item.metadata?.labels?.['tekton.dev/pipelineRun'] === name);
  const contractTaskRuns = matching.filter((item) => taskTerminalRecord(item) !== null);
  const terminalRecords = contractTaskRuns.map(taskTerminalRecord).filter(Boolean);
  const logErrors = [];
  let logs = '';
  if (collectLogs) {
    const result = cp.spawnSync('kubectl', ['-n', namespace, 'logs', '-l', `tekton.dev/pipelineRun=${name}`, '--all-containers=true', '--tail=320', '--max-log-requests=20'], {
      encoding: 'utf8',
      timeout: 12000,
      maxBuffer: 4 * 1024 * 1024,
    });
    if (result.error || result.status !== 0) {
      state.source = 'logs-unavailable';
      logErrors.push({ taskRun: null, reason: result.error ? String(result.error.message || result.error) : String(result.stderr || '').slice(0, 240) });
    } else {
      logs = result.stdout || '';
    }
  }
  const lines = logs.split(/\r?\n/u);
  const headerIndex = lines.findIndex((line) => /^ENV_REUSE\s+NODE_DEPS\s+/u.test(line.trim()));
  if (headerIndex >= 0) {
    const row = (lines.slice(headerIndex + 1).find((line) => line.trim()) || '').trim().split(/\s+/u);
    if (row.length >= 5) {
      state.status = row[0];
      state.buildCache = row[4];
    }
  }
  const records = [...parsePacLogRecords(logs), ...terminalRecords];
  for (const parsed of records) recursiveEnvReuse(parsed, state);
  state.artifact = extractPacArtifactEvidence(records, logs);
  state.sourceObservation = state.artifact.sourceObservation;
  state.collector = {
    mode: state.source,
    matchingTaskCount: matching.length,
    contractTaskCount: contractTaskRuns.length,
    terminalRecordCount: terminalRecords.length,
    logErrorCount: logErrors.length,
    logErrors,
    valuesPrinted: false,
  };
  return state;
}

function failedStep(item) {
  const steps = (item.status?.steps || []).map((step) => ({
    name: step.name || null,
    container: step.container || null,
    exitCode: Number.isInteger(step.terminated?.exitCode) ? step.terminated.exitCode : null,
    reason: step.terminated?.reason || null,
    terminationReason: step.terminationReason || null,
  }));
  return steps.find((step) => step.exitCode !== null && step.exitCode !== 0 && step.terminationReason !== 'Skipped')
    || steps.find((step) => step.terminationReason === 'Error')
    || null;
}

function taskSummary(taskRuns, name, namespace) {
  const rows = (taskRuns.items || [])
    .filter((item) => item.metadata?.labels?.['tekton.dev/pipelineRun'] === name)
    .map((item) => {
      const c = condition(item);
      return {
        name: item.metadata?.name || '',
        status: c.status || null,
        reason: c.reason || null,
        message: c.message || null,
        failedStep: failedStep(item),
        startTime: item.status?.startTime || null,
        completionTime: item.status?.completionTime || null,
        durationSeconds: durationSeconds(item.status?.startTime, item.status?.completionTime),
      };
    });
  const longest = [...rows].sort((a, b) => (b.durationSeconds || 0) - (a.durationSeconds || 0))[0] || null;
  const failed = rows.find((row) => row.status === 'False') || null;
  const target = process.env.UNIDESK_PAC_TARGET_ID || '';
  return {
    count: rows.length,
    longest,
    failed,
    details: detailId ? rows : undefined,
    logsCommand: target && namespace
      ? `trans ${target}:k3s logs -n ${namespace} -l tekton.dev/pipelineRun=${name} --all-containers --tail=240`
      : null,
  };
}

function rowFor(consumer, item, taskRuns) {
  const params = mapParams(item.spec?.params);
  const c = condition(item);
  const tasks = taskSummary(taskRuns, item.metadata.name, consumer.namespace);
  const labels = item.metadata?.labels || {};
  const annotations = item.metadata?.annotations || {};
  const sourceCommit = extractCommit(item, params);
  const evidence = envReuseForPipelineRun(consumer.namespace, item.metadata.name, taskRuns);
  const sourceObservation = evidence.sourceObservation === null ? null : {
    ...evidence.sourceObservation,
    sourceMatched: sourceCommit !== null && evidence.sourceObservation.sourceCommit === sourceCommit,
  };
  const classification = classifyPacPipelineRun(consumer, item);
  return {
    id: item.metadata.name,
    consumer: consumer.id,
    repository: consumer.repository,
    repo: consumer.repo,
    repoUrl: consumer.repoUrl,
    pipeline: consumer.pipeline,
    pipelineRun: item.metadata.name,
    triggeredAt: item.metadata?.creationTimestamp || item.status?.startTime || null,
    startTime: item.status?.startTime || null,
    completionTime: item.status?.completionTime || null,
    displayTime: formatDisplayTime(item.metadata?.creationTimestamp || item.status?.startTime || null),
    displayStartTime: formatDisplayTime(item.status?.startTime || null),
    displayCompletionTime: formatDisplayTime(item.status?.completionTime || null),
    displayTimeZone,
    durationSeconds: durationSeconds(item.status?.startTime || item.metadata?.creationTimestamp, item.status?.completionTime),
    status: c.status || null,
    reason: c.reason || null,
    commit: sourceCommit,
    branch: extractBranch(item, params),
    eventType: firstString(labels['pipelinesascode.tekton.dev/event-type'], annotations['pipelinesascode.tekton.dev/event-type']),
    sender: firstString(labels['pipelinesascode.tekton.dev/sender'], annotations['pipelinesascode.tekton.dev/sender']),
    deliveryClass: classification.deliveryClass,
    deliveryAuthorityEligible: classification.deliveryAuthorityEligible,
    deliveryOwner: classification.deliveryOwner,
    executionOwner: classification.executionOwner,
    parentPipelineRun: classification.parentPipelineRun,
    parentRelation: classification.parentRelation,
    classification,
    envReuse: {
      status: evidence.status,
      identity: evidence.identity,
      buildSkippedCount: evidence.buildSkippedCount,
      serviceReusedCount: evidence.serviceReusedCount,
      buildCache: evidence.buildCache,
      stageTimings: evidence.stageTimings,
      source: evidence.source,
    },
    sourceObservation,
    artifact: evidence.artifact,
    collector: evidence.collector,
    traceId: evidence.traceId,
    firstBreak: evidence.firstBreak,
    error: evidence.error,
    taskRuns: tasks,
    source: classification.deliveryClass === 'outer-pac-event'
      ? 'gitea-pac-event-observed'
      : classification.deliveryClass === 'inner-deterministic-publish'
        ? 'tekton-deterministic-publish-observed'
        : 'tekton-unclassified-observed',
    valuesPrinted: false,
  };
}

const rows = [];
const consumerSummaries = [];
const pipelineRunsByNamespace = new Map();
const taskRunsByNamespace = new Map();
function namespaceResources(cache, consumer, resource) {
  if (!cache.has(consumer.namespace)) {
    cache.set(consumer.namespace, kubectlJson(
      ['-n', consumer.namespace, 'get', resource, '-o', 'json'],
      { items: [] },
      `${consumer.namespace}:${resource}`,
    ));
  }
  return cache.get(consumer.namespace);
}
for (const consumer of consumers) {
  consumer.admissionState = admissionStateForConsumer(consumer);
  const pipelineRuns = namespaceResources(pipelineRunsByNamespace, consumer, 'pipelinerun');
  const taskRuns = namespaceResources(taskRunsByNamespace, consumer, 'taskrun');
  let matches = (pipelineRuns.items || []).filter((item) => {
    const name = item.metadata?.name || '';
    if (detailId) return name === detailId && pipelineRunMatchesConsumer(consumer, item);
    return pipelineRunMatchesConsumer(consumer, item);
  });
  matches = matches.sort((a, b) => Date.parse(b.metadata?.creationTimestamp || 0) - Date.parse(a.metadata?.creationTimestamp || 0));
  if (!detailId) matches = matches.slice(0, limit);
  for (const item of matches) rows.push(rowFor(consumer, item, taskRuns));
  const classified = matches.map((item) => classifyPacPipelineRun(consumer, item));
  consumerSummaries.push({
    id: consumer.id,
    namespace: consumer.namespace,
    repository: consumer.repository,
    pipelineRunPrefix: consumer.pipelineRunPrefix,
    totalPipelineRuns: (pipelineRuns.items || []).length,
    matched: matches.length,
    outerPacEvents: classified.filter((item) => item.deliveryClass === 'outer-pac-event').length,
    innerDeterministicPublishes: classified.filter((item) => item.deliveryClass === 'inner-deterministic-publish').length,
    unknown: classified.filter((item) => item.deliveryClass === 'unknown').length,
  });
}
rows.sort((a, b) => Date.parse(b.triggeredAt || 0) - Date.parse(a.triggeredAt || 0));
process.stdout.write(JSON.stringify({ rows, consumers: consumerSummaries, errors: kubectlJsonErrors }));
NODE
}

regression_rows() {
  node <<'NODE'
const cp = require('node:child_process');
const fs = require('node:fs');
const { classifyPacPipelineRun, pipelineRunMatchesConsumer } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
const limit = Math.max(1, Math.min(50, Number.parseInt(process.env.UNIDESK_PAC_HISTORY_LIMIT || '20', 10) || 20));
const consumers = JSON.parse(Buffer.from(process.env.UNIDESK_PAC_HISTORY_CONSUMERS_B64 || 'W10=', 'base64').toString('utf8'));
const errors = [];
const namespaceItems = new Map();
function pipelineRuns(namespace) {
  if (namespaceItems.has(namespace)) return namespaceItems.get(namespace);
  const tmpDir = fs.mkdtempSync('/tmp/unidesk-pac-regression-');
  const outPath = `${tmpDir}/pipelineruns.json`;
  const outFd = fs.openSync(outPath, 'w');
  const result = cp.spawnSync('kubectl', ['-n', namespace, 'get', 'pipelinerun', '-o', 'json'], { stdio: ['ignore', outFd, 'pipe'], encoding: 'utf8', timeout: 8000 });
  fs.closeSync(outFd);
  let items = [];
  if (result.error || result.status !== 0) {
    errors.push({ context: `${namespace}:pipelinerun`, status: result.status, error: result.error ? String(result.error.message || result.error).slice(0, 480) : null, stderr: String(result.stderr || '').slice(0, 480) });
  } else {
    try { items = JSON.parse(fs.readFileSync(outPath, 'utf8') || '{"items":[]}').items || []; }
    catch (error) { errors.push({ context: `${namespace}:pipelinerun-parse`, status: null, error: String(error && error.message ? error.message : error).slice(0, 480), stderr: null }); }
  }
  try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
  namespaceItems.set(namespace, items);
  return items;
}
function firstString(...values) {
  for (const value of values) if (typeof value === 'string' && value.length > 0) return value;
  return null;
}
function params(items) {
  const out = {};
  for (const item of items || []) if (item && typeof item.name === 'string') out[item.name] = typeof item.value === 'string' ? item.value : item.value == null ? null : JSON.stringify(item.value);
  return out;
}
function condition(item) {
  const value = (item.status?.conditions || []).find((candidate) => candidate.type === 'Succeeded') || {};
  return { status: value.status || null, reason: value.reason || null, message: value.message || null };
}
function safeSummary(value) {
  return typeof value !== 'string' ? null : value
    .replace(/(authorization\s*[:=]\s*)(?:bearer\s+)?\S+/giu, '$1[REDACTED]')
    .replace(/(token|password|secret)(\s*[:=]\s*)\S+/giu, '$1$2[REDACTED]')
    .replace(/https?:\/\/[^\s/]+@/giu, 'https://[REDACTED]@')
    .replace(/([?&][^=\s]+)=([^&\s]+)/gu, '$1=[REDACTED]')
    .replace(/\s+/gu, ' ').trim().slice(0, 480);
}
function commit(item, values) {
  const labels = item.metadata?.labels || {};
  const annotations = item.metadata?.annotations || {};
  return firstString(labels['pipelinesascode.tekton.dev/sha'], labels['pipelinesascode.tekton.dev/commit'], labels['unidesk.ai/source-commit'], labels['hwlab.pikastech.local/source-commit'], labels['agentrun.pikastech.local/source-commit'], annotations['pipelinesascode.tekton.dev/sha'], annotations['pipelinesascode.tekton.dev/commit'], annotations['unidesk.ai/source-commit'], values.revision, values.source_revision, values.sourceCommit, values.source_commit, values.git_sha, values.sha, values.commit);
}
function branch(item, values) {
  const labels = item.metadata?.labels || {};
  const annotations = item.metadata?.annotations || {};
  return firstString(labels['pipelinesascode.tekton.dev/branch'], annotations['pipelinesascode.tekton.dev/branch'], values.source_branch, values.branch, values.git_branch);
}
function durationSeconds(item) {
  const start = Date.parse(item.status?.startTime || item.metadata?.creationTimestamp || '');
  const end = Date.parse(item.status?.completionTime || '');
  return Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, Math.round((end - start) / 1000)) : null;
}
const rows = [];
const summaries = [];
for (const consumer of consumers) {
  const matches = pipelineRuns(consumer.namespace)
    .filter((item) => pipelineRunMatchesConsumer(consumer, item))
    .map((item) => ({ item, classification: classifyPacPipelineRun(consumer, item) }))
    .filter(({ classification }) => classification.deliveryAuthorityEligible === true)
    .sort((left, right) => Date.parse(right.item.metadata?.creationTimestamp || 0) - Date.parse(left.item.metadata?.creationTimestamp || 0))
    .slice(0, limit);
  for (const { item, classification } of matches) {
    const c = condition(item);
    const values = params(item.spec?.params);
    const failed = c.status === 'False';
    rows.push({
      id: item.metadata?.name || null,
      pipelineRun: item.metadata?.name || null,
      consumer: consumer.id,
      repository: consumer.repository,
      repo: consumer.repo,
      pipeline: consumer.pipeline,
      triggeredAt: item.metadata?.creationTimestamp || item.status?.startTime || null,
      startTime: item.status?.startTime || null,
      completionTime: item.status?.completionTime || null,
      durationSeconds: durationSeconds(item),
      status: c.status,
      reason: c.reason,
      commit: commit(item, values),
      branch: branch(item, values),
      deliveryClass: classification.deliveryClass,
      deliveryAuthorityEligible: classification.deliveryAuthorityEligible,
      deliveryOwner: classification.deliveryOwner,
      executionOwner: classification.executionOwner,
      parentRelation: classification.parentRelation,
      classification,
      traceId: firstString(item.metadata?.annotations?.['unidesk.ai/trace-id']),
      firstBreak: failed ? { code: 'tekton-pipelinerun-failed', phase: c.reason || 'pipeline-run', reason: safeSummary(c.message || c.reason || 'PipelineRun failed'), valuesPrinted: false } : null,
      error: failed ? { type: 'TektonPipelineRunError', code: c.reason || 'PipelineRunFailed', phase: c.reason || 'pipeline-run', exitCode: null, valuesPrinted: false } : null,
      source: 'pipelinerun-summary',
      valuesPrinted: false,
    });
  }
  summaries.push({ id: consumer.id, namespace: consumer.namespace, repository: consumer.repository, pipelineRunPrefix: consumer.pipelineRunPrefix, matched: matches.length, valuesPrinted: false });
}
rows.sort((left, right) => Date.parse(right.triggeredAt || 0) - Date.parse(left.triggeredAt || 0));
process.stdout.write(JSON.stringify({ rows, consumers: summaries, errors }));
NODE
}

task_rows() {
  payload_file="${UNIDESK_PAC_TASKRUN_FILE:-}"
  remove_payload=false
  if [ -z "$payload_file" ]; then
    payload_file=$(mktemp)
    remove_payload=true
    pac_read kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get taskrun -o json >"$payload_file" 2>/dev/null || printf '{"items":[]}' >"$payload_file"
  fi
  node - "$payload_file" <<'NODE'
const fs = require('node:fs');
const input = fs.readFileSync(process.argv[2], 'utf8') || '{"items":[]}';
const data = input ? JSON.parse(input) : { items: [] };
const pr = process.env.UNIDESK_PAC_TARGET_PIPELINERUN || '';
if (!pr) {
  process.stdout.write('[]');
  process.exit(0);
}
function cond(item) {
  const c = (item.status?.conditions || []).find((x) => x.type === 'Succeeded') || {};
  return { status: c.status || '', reason: c.reason || '', message: c.message || '' };
}
function failedStep(item) {
  const steps = (item.status?.steps || []).map((step) => ({
    name: step.name || null,
    exitCode: Number.isInteger(step.terminated?.exitCode) ? step.terminated.exitCode : null,
    reason: step.terminated?.reason || null,
    terminationReason: step.terminationReason || null,
  }));
  return steps.find((step) => step.exitCode !== null && step.exitCode !== 0 && step.terminationReason !== 'Skipped')
    || steps.find((step) => step.terminationReason === 'Error')
    || null;
}
function durationSeconds(item) {
  const start = item.status?.startTime;
  const done = item.status?.completionTime;
  if (!start) return null;
  return Math.max(0, Math.round(((done ? Date.parse(done) : Date.now()) - Date.parse(start)) / 1000));
}
const rows = (data.items || [])
  .filter((item) => item.metadata?.labels?.['tekton.dev/pipelineRun'] === pr)
  .sort((a, b) => Date.parse(a.metadata?.creationTimestamp || 0) - Date.parse(b.metadata?.creationTimestamp || 0))
  .slice(0, 20)
  .map((item) => {
    const c = cond(item);
    return { name: item.metadata.name, pipelineRun: item.metadata.labels?.['tekton.dev/pipelineRun'] || null, status: c.status, reason: c.reason, message: c.message || null, failedStep: failedStep(item), startTime: item.status?.startTime || null, completionTime: item.status?.completionTime || null, durationSeconds: durationSeconds(item) };
  });
process.stdout.write(JSON.stringify(rows));
NODE
  if [ "$remove_payload" = true ]; then rm -f "$payload_file"; fi
}

artifact_summary() {
  if [ -z "${UNIDESK_PAC_TARGET_PIPELINERUN:-}" ]; then
    printf '{}'
    return
  fi
  task_file="${UNIDESK_PAC_TASKRUN_FILE:-}"
  remove_task_file=false
  if [ -z "$task_file" ]; then
    task_file=$(mktemp)
    remove_task_file=true
    pac_read kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get taskrun -l "tekton.dev/pipelineRun=$UNIDESK_PAC_TARGET_PIPELINERUN" -o json >"$task_file" 2>/dev/null || printf '{"items":[]}' >"$task_file"
  fi
  node - "$task_file" <<'NODE'
const fs = require('node:fs');
const cp = require('node:child_process');
const { extractPacArtifactEvidence, parsePacLogRecords, taskTerminalRecord } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
const input = JSON.parse(fs.readFileSync(process.argv[2], 'utf8') || '{"items":[]}');
const namespace = process.env.UNIDESK_PAC_TARGET_NAMESPACE;
const pipelineRun = process.env.UNIDESK_PAC_TARGET_PIPELINERUN;
const taskRuns = (input.items || []).filter((item) => item.metadata?.labels?.['tekton.dev/pipelineRun'] === pipelineRun);
const contractTaskRuns = taskRuns.filter((item) => taskTerminalRecord(item) !== null);
const terminalRecords = contractTaskRuns.map(taskTerminalRecord).filter(Boolean);
const logErrors = [];
let logText = '';
let mode = 'taskrun-status-only';
if (process.env.UNIDESK_PAC_ACTION !== 'status' || process.env.UNIDESK_PAC_COLLECT_ARTIFACT_LOGS === '1') {
  mode = 'pipeline-run-logs';
  const result = cp.spawnSync('kubectl', ['-n', namespace, 'logs', '-l', `tekton.dev/pipelineRun=${pipelineRun}`, '--all-containers=true', '--tail=320', '--max-log-requests=20'], {
    encoding: 'utf8',
    timeout: 12000,
    maxBuffer: 4 * 1024 * 1024,
  });
  if (!result.error && result.status === 0) logText = result.stdout || '';
  else {
    mode = 'logs-unavailable';
    logErrors.push({ taskRun: null, reason: result.error ? String(result.error.message || result.error) : String(result.stderr || '').slice(0, 240) });
  }
}
const records = [...parsePacLogRecords(logText), ...terminalRecords];
const out = extractPacArtifactEvidence(records, logText);
const diagnostic = { traceId: null, firstBreak: null, error: null };
function visit(value) {
  if (value === null || value === undefined) return;
  if (Array.isArray(value)) { for (const item of value) visit(item); return; }
  if (typeof value !== 'object') return;
  if (diagnostic.traceId === null && typeof value.traceId === 'string' && /^[0-9a-f]{32}$/u.test(value.traceId)) diagnostic.traceId = value.traceId;
  if (diagnostic.firstBreak === null && value.firstBreak && typeof value.firstBreak === 'object') diagnostic.firstBreak = boundedFailureRecord(value.firstBreak);
  if (diagnostic.error === null && value.error && typeof value.error === 'object') diagnostic.error = boundedFailureRecord(value.error);
  for (const item of Object.values(value)) visit(item);
}
function boundedFailureRecord(value) {
  const input = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
  const token = (item) => typeof item === 'string' && /^[A-Za-z0-9_.-]{1,100}$/u.test(item) ? item : null;
  const summary = (item) => typeof item === 'string'
    ? item.replace(/(authorization\s*[:=]\s*)(?:bearer\s+)?\S+/giu, '$1[REDACTED]')
      .replace(/(token|password|secret)(\s*[:=]\s*)\S+/giu, '$1$2[REDACTED]')
      .replace(/https?:\/\/[^\s/]+@/giu, 'https://[REDACTED]@')
      .replace(/([?&][^=\s]+)=([^&\s]+)/gu, '$1=[REDACTED]')
      .replace(/\s+/gu, ' ').trim().slice(0, 480)
    : null;
  return { type: token(input.type), code: token(input.code), phase: token(input.phase), httpStatus: Number.isInteger(input.httpStatus) ? input.httpStatus : null, exitCode: Number.isInteger(input.exitCode) ? input.exitCode : null, reason: summary(input.reason), message: summary(input.message), stderrSummary: summary(input.stderrSummary), valuesPrinted: false };
}
for (const record of records) visit(record);
out.traceId = diagnostic.traceId;
out.firstBreak = diagnostic.firstBreak;
out.error = diagnostic.error;
out.collector = {
  mode,
  matchingTaskCount: taskRuns.length,
  contractTaskCount: contractTaskRuns.length,
  terminalRecordCount: terminalRecords.length,
  logErrorCount: logErrors.length,
  logErrors,
  valuesPrinted: false,
};
process.stdout.write(JSON.stringify(out));
NODE
  if [ "$remove_task_file" = true ]; then rm -f "$task_file"; fi
}

runtime_summary() {
  app_file="${UNIDESK_PAC_ARGO_FILE:-}"
  remove_app_file=false
  if [ -z "$app_file" ]; then
    app_file=$(mktemp)
    remove_app_file=true
    pac_read kubectl -n "$UNIDESK_PAC_ARGO_NAMESPACE" get application "$UNIDESK_PAC_ARGO_APPLICATION" -o json >"$app_file" 2>/dev/null || printf '{}' >"$app_file"
  fi
  params_file=$(mktemp)
  printf '%s' "$UNIDESK_PAC_PARAMS_JSON" >"$params_file"
  target=$(node - "$app_file" "$params_file" <<'NODE'
const fs = require('node:fs');
const app = JSON.parse(fs.readFileSync(process.argv[2], 'utf8') || '{}');
const params = JSON.parse(fs.readFileSync(process.argv[3], 'utf8') || '{}');
const prefix = process.env.UNIDESK_PAC_CONSUMER_ID === 'unidesk-host' ? 'unidesk_host_' : '';
const param = (name) => params[`${prefix}${name}`] ?? params[name];
if (param('runtime_namespace') && param('runtime_deployment')) {
  process.stdout.write(`${param('runtime_namespace')}\t${param('runtime_deployment')}`);
  process.exit(0);
}
const resource = (app.status?.resources || []).find((item) => item.kind === 'Deployment' && item.namespace && item.name);
if (resource) process.stdout.write(`${resource.namespace}\t${resource.name}`);
NODE
)
  if [ "$remove_app_file" = true ]; then rm -f "$app_file"; fi
  rm -f "$params_file"
  if [ -z "$target" ]; then
    printf '{}'
    return
  fi
  namespace=$(printf '%s' "$target" | cut -f1)
  name=$(printf '%s' "$target" | cut -f2)
  deploy_file=$(mktemp)
  pac_read kubectl -n "$namespace" get deploy -o json >"$deploy_file" 2>/dev/null || printf '{"items":[]}' >"$deploy_file"
  UNIDESK_PAC_RUNTIME_PRIMARY_DEPLOYMENT="$name" node - "$deploy_file" <<'NODE' || printf '{}'
const fs = require('node:fs');
const input = fs.readFileSync(process.argv[2], 'utf8').trim();
if (!input) {
  process.stdout.write('{}');
  process.exit(0);
}
const list = JSON.parse(input);
const items = Array.isArray(list.items) ? list.items : [];
const sourceCommit = (deploy) => {
  const labels = deploy.spec?.template?.metadata?.labels || {};
  const annotations = deploy.spec?.template?.metadata?.annotations || {};
  return labels['unidesk.ai/source-commit']
    || labels['hwlab.pikastech.local/source-commit']
    || labels['agentrun.pikastech.local/source-commit']
    || annotations['unidesk.ai/source-commit']
    || annotations['hwlab.pikastech.local/source-commit']
    || annotations['agentrun.pikastech.local/source-commit']
    || null;
};
const workload = (deploy) => {
  const image = deploy.spec?.template?.spec?.containers?.[0]?.image || null;
  const digest = image && String(image).includes('@') ? String(image).split('@').slice(1).join('@') : null;
  return {
    namespace: deploy.metadata?.namespace || null,
    deployment: deploy.metadata?.name || null,
    readyReplicas: deploy.status?.readyReplicas ?? 0,
    updatedReplicas: deploy.status?.updatedReplicas ?? 0,
    replicas: deploy.spec?.replicas ?? null,
    generation: deploy.metadata?.generation ?? null,
    observedGeneration: deploy.status?.observedGeneration ?? null,
    sourceCommit: sourceCommit(deploy),
    image,
    digest,
    valuesPrinted: false,
  };
};
const workloads = items.map(workload).sort((left, right) => String(left.deployment).localeCompare(String(right.deployment))).slice(0, 100);
const primary = workloads.find((item) => item.deployment === process.env.UNIDESK_PAC_RUNTIME_PRIMARY_DEPLOYMENT) || {};
process.stdout.write(JSON.stringify({
  ...primary,
  namespace: primary.namespace || process.env.UNIDESK_PAC_RUNTIME_NAMESPACE || null,
  workloads,
  workloadCount: workloads.length,
  valuesPrinted: false,
}));
NODE
  rm -f "$deploy_file"
}

resolve_git_service_url() {
  repo_url="$1"
  host=$(UNIDESK_PAC_GIT_URL="$repo_url" node <<'NODE'
try {
  process.stdout.write(new URL(process.env.UNIDESK_PAC_GIT_URL || '').hostname);
} catch {}
NODE
)
  case "$host" in
    *.svc.cluster.local)
      service=${host%%.*}
      rest=${host#*.}
      namespace=${rest%%.*}
      cluster_ip=$(pac_read kubectl -n "$namespace" get svc "$service" -o jsonpath='{.spec.clusterIP}' 2>/dev/null || true)
      if [ -z "$cluster_ip" ]; then
        return 1
      fi
      UNIDESK_PAC_GIT_URL="$repo_url" UNIDESK_PAC_GIT_CLUSTER_IP="$cluster_ip" node <<'NODE'
try {
  const url = new URL(process.env.UNIDESK_PAC_GIT_URL || '');
  url.hostname = process.env.UNIDESK_PAC_GIT_CLUSTER_IP || '';
  process.stdout.write(url.toString());
} catch {
  process.exit(1);
}
NODE
      ;;
    *) printf '%s' "$repo_url" ;;
  esac
}

gitops_revision_relation() {
  artifact_json="${1:-}"
  argo_json="${2:-}"
  [ -n "$artifact_json" ] || artifact_json='{}'
  [ -n "$argo_json" ] || argo_json='{}'
  fields=$(UNIDESK_PAC_ARTIFACT_JSON="$artifact_json" UNIDESK_PAC_ARGO_JSON="$argo_json" node <<'NODE'
function parse(value) {
  try { return JSON.parse(value || '{}'); } catch { return {}; }
}
function line(key, value) {
  process.stdout.write(`${key}=${Buffer.from(String(value || ''), 'utf8').toString('base64')}\n`);
}
const artifact = parse(process.env.UNIDESK_PAC_ARTIFACT_JSON);
const argo = parse(process.env.UNIDESK_PAC_ARGO_JSON);
line('expected', artifact.gitopsCommit);
line('observed', argo.revision);
line('repoUrl', argo.repoURL);
line('targetRevision', argo.targetRevision);
line('retained', artifact.sourceObservation?.mode === 'no-runtime-change' && artifact.sourceObservation?.valid === true ? 'true' : 'false');
NODE
)
  expected=$(printf '%s\n' "$fields" | sed -n 's/^expected=//p' | base64 -d 2>/dev/null || true)
  observed=$(printf '%s\n' "$fields" | sed -n 's/^observed=//p' | base64 -d 2>/dev/null || true)
  repo_url=$(printf '%s\n' "$fields" | sed -n 's/^repoUrl=//p' | base64 -d 2>/dev/null || true)
  target_revision=$(printf '%s\n' "$fields" | sed -n 's/^targetRevision=//p' | base64 -d 2>/dev/null || true)
  expected_repo_url="${UNIDESK_PAC_GITOPS_EXPECTED_REPO_URL:-}"
  expected_target_revision="${UNIDESK_PAC_GITOPS_EXPECTED_TARGET_REVISION:-}"
  [ -n "$expected_repo_url" ] || expected_repo_url="$repo_url"
  [ -n "$expected_target_revision" ] || expected_target_revision="$target_revision"
  normalized_expected_repo=$(printf '%s' "$expected_repo_url" | sed 's#/*$##')
  normalized_observed_repo=$(printf '%s' "$repo_url" | sed 's#/*$##')
  retained=$(printf '%s\n' "$fields" | sed -n 's/^retained=//p' | base64 -d 2>/dev/null || true)
  relation=unknown
  proof=unavailable
  reason=missing-revision-context
  fetch_ok=false
  if [ "$normalized_expected_repo" != "$normalized_observed_repo" ]; then
    reason=repository-mismatch
  elif [ "$expected_target_revision" != "$target_revision" ]; then
    reason=target-revision-mismatch
  elif [ "$retained" = true ] && printf '%s' "$observed" | grep -Eq '^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$'; then
    relation=retained
    proof=structured-no-runtime-change-plan
    reason=source-observation-retains-current-revision
  elif printf '%s\n%s\n' "$expected" "$observed" | grep -Eqv '^[0-9a-fA-F]{7,64}$'; then
    reason=invalid-or-missing-revision
  elif [ -z "$expected_repo_url" ] \
    || ! git check-ref-format "refs/heads/$expected_target_revision" >/dev/null 2>&1; then
    reason=invalid-or-missing-git-context
  else
    repo_dir=$(mktemp -d)
    if resolved_url=$(resolve_git_service_url "$expected_repo_url") \
      && git init --bare -q "$repo_dir/repo.git" \
      && timeout "$UNIDESK_PAC_WAIT_TIMEOUT_SECONDS" git --git-dir="$repo_dir/repo.git" fetch -q --no-tags "$resolved_url" "+refs/heads/$expected_target_revision:refs/remotes/origin/$expected_target_revision" >/dev/null 2>&1; then
      fetch_ok=true
      if git --git-dir="$repo_dir/repo.git" cat-file -e "$expected^{commit}" 2>/dev/null \
        && git --git-dir="$repo_dir/repo.git" cat-file -e "$observed^{commit}" 2>/dev/null; then
        proof=owning-git-commit-graph
        expected=$(git --git-dir="$repo_dir/repo.git" rev-parse "$expected^{commit}")
        observed=$(git --git-dir="$repo_dir/repo.git" rev-parse "$observed^{commit}")
        if [ "$expected" = "$observed" ]; then
          relation=exact
          reason=commits-equal
        elif git --git-dir="$repo_dir/repo.git" merge-base --is-ancestor "$expected" "$observed" >/dev/null 2>&1; then
          relation=descendant
          reason=observed-descends-from-selected
        elif git --git-dir="$repo_dir/repo.git" merge-base --is-ancestor "$observed" "$expected" >/dev/null 2>&1; then
          relation=stale
          reason=observed-precedes-selected
        else
          relation=diverged
          reason=commits-have-diverged
        fi
      else
        reason=commit-object-missing
      fi
    else
      reason=owning-git-fetch-failed
    fi
    rm -rf "$repo_dir"
  fi
  UNIDESK_PAC_GITOPS_EXPECTED="$expected" \
  UNIDESK_PAC_GITOPS_OBSERVED="$observed" \
  UNIDESK_PAC_GITOPS_REPO_URL="$repo_url" \
  UNIDESK_PAC_GITOPS_TARGET_REVISION="$target_revision" \
  UNIDESK_PAC_GITOPS_EXPECTED_REPO_URL_OBSERVED="$expected_repo_url" \
  UNIDESK_PAC_GITOPS_EXPECTED_TARGET_REVISION_OBSERVED="$expected_target_revision" \
  UNIDESK_PAC_GITOPS_RELATION="$relation" \
  UNIDESK_PAC_GITOPS_PROOF="$proof" \
  UNIDESK_PAC_GITOPS_REASON="$reason" \
  UNIDESK_PAC_GITOPS_FETCH_OK="$fetch_ok" node <<'NODE'
const { normalizeGitopsRevisionRelation } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
process.stdout.write(JSON.stringify(normalizeGitopsRevisionRelation({
  relation: process.env.UNIDESK_PAC_GITOPS_RELATION || 'unknown',
  expectedRevision: process.env.UNIDESK_PAC_GITOPS_EXPECTED || null,
  observedRevision: process.env.UNIDESK_PAC_GITOPS_OBSERVED || null,
  repoURL: process.env.UNIDESK_PAC_GITOPS_REPO_URL || null,
  targetRevision: process.env.UNIDESK_PAC_GITOPS_TARGET_REVISION || null,
  expectedRepoURL: process.env.UNIDESK_PAC_GITOPS_EXPECTED_REPO_URL_OBSERVED || null,
  expectedTargetRevision: process.env.UNIDESK_PAC_GITOPS_EXPECTED_TARGET_REVISION_OBSERVED || null,
  observedRepoURL: process.env.UNIDESK_PAC_GITOPS_REPO_URL || null,
  observedTargetRevision: process.env.UNIDESK_PAC_GITOPS_TARGET_REVISION || null,
  proof: process.env.UNIDESK_PAC_GITOPS_PROOF || 'unavailable',
  reason: process.env.UNIDESK_PAC_GITOPS_REASON || null,
  fetchOk: process.env.UNIDESK_PAC_GITOPS_FETCH_OK === 'true',
  valuesPrinted: false,
})));
NODE
}

cicd_diagnostics() {
  params_file=$(mktemp)
  pipelines_file=$(mktemp)
  artifact_file=$(mktemp)
  argo_file=$(mktemp)
  runtime_file=$(mktemp)
  printf '%s' "$UNIDESK_PAC_PARAMS_JSON" >"$params_file"
  printf '%s' "${1:-[]}" >"$pipelines_file"
  printf '%s' "${2:-{}}" >"$artifact_file"
  printf '%s' "${3:-{}}" >"$argo_file"
  printf '%s' "${4:-{}}" >"$runtime_file"
  probe_env=$(node - "$params_file" "$pipelines_file" "$artifact_file" <<'NODE'
const fs = require('node:fs');
function parseLoose(path, fallback) {
  const input = fs.readFileSync(path, 'utf8').trim();
  if (!input) return fallback;
  for (let end = input.length; end > 0; end -= 1) {
    try { return JSON.parse(input.slice(0, end)); } catch {}
  }
  return fallback;
}
const params = parseLoose(process.argv[2], {});
const pipelines = parseLoose(process.argv[3], []);
const artifact = parseLoose(process.argv[4], {});
const latest = Array.isArray(pipelines) ? pipelines[0] || {} : {};
const sourceCommit = latest.sourceCommit || null;
const noRuntimeChange = artifact.sourceObservation?.mode === 'no-runtime-change' && artifact.sourceObservation?.valid === true;
const runtimeSourceCommit = artifact.runtimeSourceCommit || (noRuntimeChange ? null : sourceCommit);
const tag = !noRuntimeChange && runtimeSourceCommit ? String(runtimeSourceCommit).slice(0, 12) : '';
const prefix = process.env.UNIDESK_PAC_CONSUMER_ID === 'unidesk-host' ? 'unidesk_host_' : '';
const param = (name) => params[`${prefix}${name}`] ?? params[name];
const registryConfigured = process.env.UNIDESK_PAC_REGISTRY_APPLICABILITY === 'configured';
const imageRepository = registryConfigured && typeof param('image_repository') === 'string' ? param('image_repository') : '';
const probeBase = registryConfigured && typeof param('registry_probe_base') === 'string' ? param('registry_probe_base').replace(/\/+$/u, '') : '';
let registryUrl = '';
if (imageRepository && tag) {
  const firstSlash = imageRepository.indexOf('/');
  const registry = firstSlash >= 0 ? imageRepository.slice(0, firstSlash) : imageRepository;
  const path = firstSlash >= 0 ? imageRepository.slice(firstSlash + 1) : '';
  const base = probeBase || (registry ? `http://${registry}` : '');
  registryUrl = base && path ? `${base}/v2/${path}/manifests/${tag}` : '';
}
function line(key, value) {
  process.stdout.write(`${key}=${Buffer.from(String(value || ''), 'utf8').toString('base64')}\n`);
}
line('imageRepository', imageRepository);
line('registryProbeBase', probeBase);
line('sourceCommit', sourceCommit || '');
line('runtimeSourceCommit', runtimeSourceCommit || '');
line('tag', tag);
line('registryUrl', registryUrl);
line('sentinelId', param('sentinel_id') || '');
line('gitopsBranch', param('gitops_branch') || '');
line('gitopsManifestPath', param('gitops_manifest_path') || '');
line('runtimeNamespace', param('runtime_namespace') || '');
line('runtimeService', param('runtime_service') || '');
line('runtimeServicePort', param('runtime_service_port') || '');
line('healthPath', param('health_path') || '');
line('healthUrl', param('health_url') || '');
NODE
)
  image_repository=$(printf '%s\n' "$probe_env" | sed -n 's/^imageRepository=//p' | base64 -d 2>/dev/null || true)
  registry_probe_base=$(printf '%s\n' "$probe_env" | sed -n 's/^registryProbeBase=//p' | base64 -d 2>/dev/null || true)
  source_commit=$(printf '%s\n' "$probe_env" | sed -n 's/^sourceCommit=//p' | base64 -d 2>/dev/null || true)
  runtime_source_commit=$(printf '%s\n' "$probe_env" | sed -n 's/^runtimeSourceCommit=//p' | base64 -d 2>/dev/null || true)
  image_tag=$(printf '%s\n' "$probe_env" | sed -n 's/^tag=//p' | base64 -d 2>/dev/null || true)
  registry_url=$(printf '%s\n' "$probe_env" | sed -n 's/^registryUrl=//p' | base64 -d 2>/dev/null || true)
  sentinel_id=$(printf '%s\n' "$probe_env" | sed -n 's/^sentinelId=//p' | base64 -d 2>/dev/null || true)
  gitops_branch=$(printf '%s\n' "$probe_env" | sed -n 's/^gitopsBranch=//p' | base64 -d 2>/dev/null || true)
  gitops_manifest_path=$(printf '%s\n' "$probe_env" | sed -n 's/^gitopsManifestPath=//p' | base64 -d 2>/dev/null || true)
  runtime_namespace=$(printf '%s\n' "$probe_env" | sed -n 's/^runtimeNamespace=//p' | base64 -d 2>/dev/null || true)
  runtime_service=$(printf '%s\n' "$probe_env" | sed -n 's/^runtimeService=//p' | base64 -d 2>/dev/null || true)
  runtime_service_port=$(printf '%s\n' "$probe_env" | sed -n 's/^runtimeServicePort=//p' | base64 -d 2>/dev/null || true)
  health_path=$(printf '%s\n' "$probe_env" | sed -n 's/^healthPath=//p' | base64 -d 2>/dev/null || true)
  health_url=$(printf '%s\n' "$probe_env" | sed -n 's/^healthUrl=//p' | base64 -d 2>/dev/null || true)
  registry_present=false
  registry_digest=""
  if [ -n "$registry_url" ]; then
    header_file=$(mktemp)
    if curl -fsS --max-time "$UNIDESK_PAC_READ_STEP_TIMEOUT_SECONDS" -D "$header_file" -o /dev/null -H 'Accept: application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.list.v2+json' "$registry_url" >/dev/null 2>&1; then
      registry_present=true
    fi
    headers=$(cat "$header_file" 2>/dev/null || true)
    rm -f "$header_file"
    if printf '%s' "$headers" | grep -qi '^HTTP/.* 200'; then registry_present=true; fi
    registry_digest=$(printf '%s\n' "$headers" | awk 'BEGIN{IGNORECASE=1} /^Docker-Content-Digest:/ { sub(/\r$/,"",$2); print $2; exit }')
  fi
  registry_present_json=false
  if [ "$registry_present" = true ]; then registry_present_json=true; fi
  health_status=""
  if [ -n "$health_url" ]; then
    if [ -n "$runtime_namespace" ] && [ -n "$runtime_service" ] && [ -n "$runtime_service_port" ] && [ -n "$health_path" ]; then
      health_file=$(mktemp)
      if pac_read kubectl get --raw "/api/v1/namespaces/$runtime_namespace/services/$runtime_service:$runtime_service_port/proxy$health_path" >"$health_file" 2>/dev/null \
        && node - "$health_file" <<'NODE'
const fs = require('node:fs');
const body = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
process.exit(body?.ok === true ? 0 : 1);
NODE
      then
        health_status=200
      else
        health_status=503
      fi
      rm -f "$health_file"
    else
      health_status=$(curl -sS -o /dev/null -w '%{http_code}' --max-time "$UNIDESK_PAC_READ_STEP_TIMEOUT_SECONDS" "$health_url" 2>/dev/null || true)
    fi
  fi
  export UNIDESK_PAC_DIAG_IMAGE_REPOSITORY="$image_repository"
  export UNIDESK_PAC_DIAG_REGISTRY_PROBE_BASE="$registry_probe_base"
  export UNIDESK_PAC_DIAG_SOURCE_COMMIT="$source_commit"
  export UNIDESK_PAC_DIAG_RUNTIME_SOURCE_COMMIT="$runtime_source_commit"
  export UNIDESK_PAC_DIAG_IMAGE_TAG="$image_tag"
  export UNIDESK_PAC_DIAG_REGISTRY_URL="$registry_url"
  export UNIDESK_PAC_DIAG_REGISTRY_PRESENT="$registry_present_json"
  export UNIDESK_PAC_DIAG_REGISTRY_DIGEST="$registry_digest"
  export UNIDESK_PAC_DIAG_SENTINEL_ID="$sentinel_id"
  export UNIDESK_PAC_DIAG_GITOPS_BRANCH="$gitops_branch"
  export UNIDESK_PAC_DIAG_GITOPS_MANIFEST_PATH="$gitops_manifest_path"
  export UNIDESK_PAC_DIAG_HEALTH_URL="$health_url"
  export UNIDESK_PAC_DIAG_HEALTH_STATUS="$health_status"
  node - "$params_file" "$pipelines_file" "$artifact_file" "$argo_file" "$runtime_file" <<'NODE'
const fs = require('node:fs');
const { evaluatePacStatus } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
function parseLoose(path, fallback) {
  const input = fs.readFileSync(path, 'utf8').trim();
  if (!input) return fallback;
  for (let end = input.length; end > 0; end -= 1) {
    try { return JSON.parse(input.slice(0, end)); } catch {}
  }
  return fallback;
}
const pipelines = parseLoose(process.argv[3], []);
const artifact = parseLoose(process.argv[4], {});
const argo = parseLoose(process.argv[5], {});
const runtime = parseLoose(process.argv[6], {});
const latest = Array.isArray(pipelines) ? pipelines[0] || {} : {};
const evaluated = evaluatePacStatus({
  sourceCommit: process.env.UNIDESK_PAC_DIAG_SOURCE_COMMIT || latest.sourceCommit || null,
  runtimeSourceCommit: process.env.UNIDESK_PAC_DIAG_RUNTIME_SOURCE_COMMIT || artifact.runtimeSourceCommit || null,
  imageRepository: process.env.UNIDESK_PAC_DIAG_IMAGE_REPOSITORY || null,
  registryApplicability: process.env.UNIDESK_PAC_REGISTRY_APPLICABILITY || 'not-configured',
  registryProbeBase: process.env.UNIDESK_PAC_DIAG_REGISTRY_PROBE_BASE || null,
  imageTag: process.env.UNIDESK_PAC_DIAG_IMAGE_TAG || null,
  registryUrl: process.env.UNIDESK_PAC_DIAG_REGISTRY_URL || null,
  registryPresent: process.env.UNIDESK_PAC_DIAG_REGISTRY_PRESENT === 'true',
  registryDigest: process.env.UNIDESK_PAC_DIAG_REGISTRY_DIGEST || null,
  gitopsBranch: process.env.UNIDESK_PAC_DIAG_GITOPS_BRANCH || null,
  gitopsManifestPath: process.env.UNIDESK_PAC_DIAG_GITOPS_MANIFEST_PATH || null,
  healthUrl: process.env.UNIDESK_PAC_DIAG_HEALTH_URL || null,
  healthStatus: process.env.UNIDESK_PAC_DIAG_HEALTH_STATUS || null,
  pipelineRun: latest.name || null,
  artifact,
  argo,
  runtime,
});
process.stdout.write(JSON.stringify(evaluated));
NODE
  rm -f "$params_file" "$pipelines_file" "$artifact_file" "$argo_file" "$runtime_file"
}

hook_summary() {
  hooks=$(gitea_api GET "repos/$UNIDESK_PAC_GITEA_OWNER/$UNIDESK_PAC_GITEA_REPO/hooks" 2>/dev/null || echo '[]')
  HOOKS_JSON="$hooks" node <<'NODE'
const data = JSON.parse(process.env.HOOKS_JSON || '[]');
const url = process.env.UNIDESK_PAC_WEBHOOK_URL;
const rows = data.filter((item) => item?.config?.url === url).map((item) => ({ id: item.id, active: item.active, type: item.type, events: item.events || [], url: item.config?.url || '' }));
process.stdout.write(JSON.stringify(rows));
NODE
}

consumer_bootstrap_state() {
  if [ -z "${UNIDESK_PAC_RUNNER_SERVICE_ACCOUNT_NAME:-}" ] && [ -z "${UNIDESK_PAC_ARGO_REPOSITORY_SECRET_NAME:-}" ]; then
    printf '{"configured":false,"ready":null,"reasons":["consumer-bootstrap-not-declared"],"valuesPrinted":false}'
    return
  fi
  sa_file=$(mktemp)
  role_binding_file=$(mktemp)
  argo_secret_file=$(mktemp)
  if [ -n "${UNIDESK_PAC_RUNNER_SERVICE_ACCOUNT_NAME:-}" ]; then
    pac_read kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get serviceaccount "$UNIDESK_PAC_RUNNER_SERVICE_ACCOUNT_NAME" -o json >"$sa_file" 2>/dev/null || printf '{}' >"$sa_file"
    pac_read kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get rolebinding "$UNIDESK_PAC_RUNNER_ROLE_BINDING_NAME" -o json >"$role_binding_file" 2>/dev/null || printf '{}' >"$role_binding_file"
  else
    printf '{}' >"$sa_file"
    printf '{}' >"$role_binding_file"
  fi
  if [ -n "${UNIDESK_PAC_ARGO_REPOSITORY_SECRET_NAME:-}" ]; then
    pac_read kubectl -n "$UNIDESK_PAC_ARGO_NAMESPACE" get secret "$UNIDESK_PAC_ARGO_REPOSITORY_SECRET_NAME" -o json >"$argo_secret_file" 2>/dev/null || printf '{}' >"$argo_secret_file"
  else
    printf '{}' >"$argo_secret_file"
  fi
  node - "$sa_file" "$role_binding_file" "$argo_secret_file" <<'NODE'
const crypto = require('node:crypto');
const fs = require('node:fs');
function read(path) {
  try { return JSON.parse(fs.readFileSync(path, 'utf8') || '{}'); } catch { return {}; }
}
function fingerprint(value) {
  return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
}
const sa = read(process.argv[2]);
const roleBinding = read(process.argv[3]);
const argoSecret = read(process.argv[4]);
const namespace = process.env.UNIDESK_PAC_TARGET_NAMESPACE || '';
const runnerName = process.env.UNIDESK_PAC_RUNNER_SERVICE_ACCOUNT_NAME || '';
const roleBindingName = process.env.UNIDESK_PAC_RUNNER_ROLE_BINDING_NAME || '';
const argoNamespace = process.env.UNIDESK_PAC_ARGO_NAMESPACE || '';
const argoSecretName = process.env.UNIDESK_PAC_ARGO_REPOSITORY_SECRET_NAME || '';
const argoUrl = process.env.UNIDESK_PAC_ARGO_REPOSITORY_URL || '';
const reasons = [];
const runnerConfigured = runnerName.length > 0;
const saExists = !runnerConfigured || sa.metadata?.name === runnerName;
const automountDisabled = !runnerConfigured || sa.automountServiceAccountToken === false;
const subjects = Array.isArray(roleBinding.subjects) ? roleBinding.subjects : [];
const roleBindingExact = !runnerConfigured || (
  roleBinding.metadata?.name === roleBindingName
  && roleBinding.metadata?.namespace === namespace
  && subjects.length === 1
  && subjects[0]?.kind === 'ServiceAccount'
  && subjects[0]?.name === runnerName
  && subjects[0]?.namespace === namespace
  && roleBinding.roleRef?.apiGroup === 'rbac.authorization.k8s.io'
  && roleBinding.roleRef?.kind === 'Role'
  && roleBinding.roleRef?.name === 'unidesk-pac-consumer-runner'
);
if (!saExists) reasons.push('runner-service-account-missing');
if (!automountDisabled) reasons.push('runner-service-account-automount-not-false');
if (!roleBindingExact) reasons.push('runner-rolebinding-drifted-or-missing');
const argoConfigured = argoSecretName.length > 0;
const argoExists = !argoConfigured || (argoSecret.metadata?.name === argoSecretName && argoSecret.metadata?.namespace === argoNamespace);
const argoLabelReady = !argoConfigured || argoSecret.metadata?.labels?.['argocd.argoproj.io/secret-type'] === 'repository';
const keys = Object.keys(argoSecret.data || {}).sort();
const expectedKeys = ['password', 'url', 'username'];
const keysReady = !argoConfigured || JSON.stringify(keys) === JSON.stringify(expectedKeys);
let observedUrlFingerprint = null;
if (argoConfigured && typeof argoSecret.data?.url === 'string') {
  try { observedUrlFingerprint = fingerprint(Buffer.from(argoSecret.data.url, 'base64').toString('utf8')); } catch {}
}
const expectedUrlFingerprint = argoConfigured ? fingerprint(argoUrl) : null;
const urlReady = !argoConfigured || observedUrlFingerprint === expectedUrlFingerprint;
const passwordPresent = !argoConfigured || (typeof argoSecret.data?.password === 'string' && argoSecret.data.password.length > 0);
if (!argoExists) reasons.push('argocd-repository-secret-missing');
if (!argoLabelReady) reasons.push('argocd-repository-secret-label-drifted');
if (!keysReady) reasons.push('argocd-repository-secret-keys-drifted');
if (!urlReady) reasons.push('argocd-repository-secret-url-drifted');
if (!passwordPresent) reasons.push('argocd-repository-secret-password-missing');
process.stdout.write(JSON.stringify({
  configured: runnerConfigured || argoConfigured,
  ready: reasons.length === 0,
  blocking: false,
  warning: reasons.length > 0,
  warnings: reasons,
  runner: {
    configured: runnerConfigured,
    serviceAccount: runnerName || null,
    exists: saExists,
    automountServiceAccountToken: sa.metadata?.name === runnerName ? sa.automountServiceAccountToken ?? null : null,
    automountDisabled,
    roleBinding: roleBindingName || null,
    roleBindingExact,
  },
  argoRepository: {
    configured: argoConfigured,
    secretName: argoSecretName || null,
    exists: argoExists,
    labelReady: argoLabelReady,
    expectedKeys,
    observedKeys: keys,
    keysReady,
    expectedUrlFingerprint,
    observedUrlFingerprint,
    urlReady,
    passwordPresent,
    passwordDecoded: false,
  },
  reasons,
  valuesPrinted: false,
}));
NODE
  rm -f "$sa_file" "$role_binding_file" "$argo_secret_file"
}

status_progress() {
  elapsed_seconds=$(( $(now_epoch) - UNIDESK_PAC_STATUS_STARTED_EPOCH ))
  printf '{"event":"pac-status-progress","stage":"%s","elapsedSeconds":%s,"stepTimeoutSeconds":%s,"valuesPrinted":false}\n' "$1" "$elapsed_seconds" "$UNIDESK_PAC_READ_STEP_TIMEOUT_SECONDS" >&2
}

status_action() {
  UNIDESK_PAC_STATUS_STARTED_EPOCH=$(now_epoch)
  status_progress control-plane
  crd=$(pac_read kubectl get crd repositories.pipelinesascode.tekton.dev -o name 2>/dev/null || true)
  controller_ready=$(pac_read kubectl -n "$UNIDESK_PAC_RELEASE_NAMESPACE" get deploy "$UNIDESK_PAC_CONTROLLER_SERVICE_NAME" -o jsonpath='{.status.readyReplicas}/{.spec.replicas}' 2>/dev/null || echo "0/0")
  repository_condition=$(condition_status "$UNIDESK_PAC_TARGET_NAMESPACE" repository "$UNIDESK_PAC_REPOSITORY_NAME")
  status_progress admission-provenance
  prepare_admission_provenance_state
  status_progress consumer-bootstrap
  consumer_bootstrap=$(consumer_bootstrap_state)
  bootstrap_ready=$(UNIDESK_PAC_BOOTSTRAP_STATE="$consumer_bootstrap" node -e 'const s=JSON.parse(process.env.UNIDESK_PAC_BOOTSTRAP_STATE||"{}"); process.stdout.write(s.configured===true&&s.ready!==true?"false":"true")')
  status_progress pipeline-runs
  if ! pipelines=$(pipeline_rows); then
    status_progress pipeline-runs-read-failed
    printf '{"ok":false,"action":"platform-infra-pipelines-as-code-status","mutation":false,"crdPresent":%s,"controllerReady":"%s","repositoryCondition":"%s","admissionProvenance":%s,"consumerBootstrap":%s,"error":"pac-pipelinerun-read-failed","errorStage":"pipeline-runs","valuesPrinted":false}\n' \
      "$([ -n "$crd" ] && echo true || echo false)" \
      "$(json_string "$controller_ready")" \
      "$(json_string "$repository_condition")" \
      "$UNIDESK_PAC_ADMISSION_STATE_JSON" \
      "$consumer_bootstrap"
    return 1
  fi
  latest=$(printf '%s' "$pipelines" | node -e 'const fs=require("fs"); const a=JSON.parse(fs.readFileSync(0,"utf8")||"[]"); process.stdout.write(a[0]?.name||"")')
  export UNIDESK_PAC_TARGET_PIPELINERUN="$latest"
  status_progress task-runs
  task_file=$(mktemp)
  if [ -n "$latest" ]; then
    pac_read kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get taskrun -l "tekton.dev/pipelineRun=$latest" -o json >"$task_file" 2>/dev/null || printf '{"items":[]}' >"$task_file"
  else
    printf '{"items":[]}' >"$task_file"
  fi
  export UNIDESK_PAC_TASKRUN_FILE="$task_file"
  tasks=$(task_rows)
  status_progress artifact-evidence
  artifact=$(json_normalize "$(artifact_summary)")
  unset UNIDESK_PAC_TASKRUN_FILE
  rm -f "$task_file"
  status_progress webhooks
  hooks=$(hook_summary)
  status_progress argo
  argo_file=$(mktemp)
  pac_read kubectl -n "$UNIDESK_PAC_ARGO_NAMESPACE" get application "$UNIDESK_PAC_ARGO_APPLICATION" -o json >"$argo_file" 2>/dev/null || printf '{}' >"$argo_file"
  export UNIDESK_PAC_ARGO_FILE="$argo_file"
  argo=$(node -e 'const fs=require("fs"); const s=fs.readFileSync(0,"utf8").trim(); if(!s){process.stdout.write("{}"); process.exit(0)} const a=JSON.parse(s); const source=a.spec?.source || a.spec?.sources?.[0] || {}; let repoURL=source.repoURL||null; try { const url=new URL(repoURL); url.username=""; url.password=""; repoURL=url.toString(); } catch {} const bounded=(value)=>typeof value==="string"?value.replace(/(authorization\\s*[:=]\\s*)(?:bearer\\s+)?\\S+/giu,"$1[REDACTED]").replace(/(token|password|secret)(\\s*[:=]\\s*)\\S+/giu,"$1$2[REDACTED]").replace(/\\s+/gu," ").trim().slice(0,480):null; const resources=a.status?.resources||[]; const degraded=resources.find((item)=>item.status==="OutOfSync"||item.health?.status==="Degraded"||item.health?.status==="Missing")||resources.find((item)=>item.status!=="Synced"||(item.health?.status&&item.health.status!=="Healthy"))||null; const condition=(a.status?.conditions||[])[0]||((a.status?.operationState?.phase&&a.status.operationState.phase!=="Succeeded")?{type:a.status.operationState.phase,message:a.status.operationState.message}:null); process.stdout.write(JSON.stringify({sync:a.status?.sync?.status||null, health:a.status?.health?.status||null, revision:a.status?.sync?.revision||null, repoURL, targetRevision:source.targetRevision||null, firstDegradedResource:degraded?{group:degraded.group||null,kind:degraded.kind||null,namespace:degraded.namespace||null,name:degraded.name||null,status:degraded.status||null,health:degraded.health?.status||null}:null,firstCondition:condition?{type:condition.type||null,message:bounded(condition.message)}:null}))' <"$argo_file" || echo '{}')
  argo=$(json_normalize "$argo")
  status_progress gitops-revision
  revision_relation=$(gitops_revision_relation "$artifact" "$argo")
  artifact=$(UNIDESK_PAC_ARTIFACT_JSON="$artifact" UNIDESK_PAC_REVISION_RELATION_JSON="$revision_relation" node <<'NODE'
const artifact = JSON.parse(process.env.UNIDESK_PAC_ARTIFACT_JSON || '{}');
const revisionRelation = JSON.parse(process.env.UNIDESK_PAC_REVISION_RELATION_JSON || '{}');
process.stdout.write(JSON.stringify({ ...artifact, gitopsCommit: revisionRelation.expectedRevision || artifact.gitopsCommit || null }));
NODE
)
  argo=$(UNIDESK_PAC_ARGO_JSON="$argo" UNIDESK_PAC_REVISION_RELATION_JSON="$revision_relation" node <<'NODE'
const argo = JSON.parse(process.env.UNIDESK_PAC_ARGO_JSON || '{}');
const revisionRelation = JSON.parse(process.env.UNIDESK_PAC_REVISION_RELATION_JSON || '{"relation":"unknown"}');
process.stdout.write(JSON.stringify({ ...argo, revisionRelation }));
NODE
)
  status_progress runtime
  runtime=$(json_normalize "$(runtime_summary)")
  unset UNIDESK_PAC_ARGO_FILE
  rm -f "$argo_file"
  status_progress diagnostics
  diagnostics=$(cicd_diagnostics "$pipelines" "$artifact" "$argo" "$runtime")
  diagnostics=$(UNIDESK_PAC_DIAGNOSTICS="$diagnostics" UNIDESK_PAC_STATE="$UNIDESK_PAC_ADMISSION_STATE_JSON" UNIDESK_PAC_BOOTSTRAP_STATE="$consumer_bootstrap" UNIDESK_PAC_PIPELINES_JSON="$pipelines" UNIDESK_PAC_ARTIFACT_JSON="$artifact" UNIDESK_PAC_ARGO_JSON="$argo" UNIDESK_PAC_RUNTIME_JSON="$runtime" node <<'NODE'
const diagnostics = JSON.parse(process.env.UNIDESK_PAC_DIAGNOSTICS || '{}');
const admissionProvenance = JSON.parse(process.env.UNIDESK_PAC_STATE || '{}');
const consumerBootstrap = JSON.parse(process.env.UNIDESK_PAC_BOOTSTRAP_STATE || '{}');
const pipelineRuns = JSON.parse(process.env.UNIDESK_PAC_PIPELINES_JSON || '[]');
const artifact = JSON.parse(process.env.UNIDESK_PAC_ARTIFACT_JSON || '{}');
const argo = JSON.parse(process.env.UNIDESK_PAC_ARGO_JSON || '{}');
const runtime = JSON.parse(process.env.UNIDESK_PAC_RUNTIME_JSON || '{}');
const output = { ...diagnostics, admissionProvenance, consumerBootstrap, valuesPrinted: false };
const latest = Array.isArray(pipelineRuns) ? pipelineRuns[0] || {} : {};
const sourceWorkloads = Array.isArray(runtime.workloads)
  ? runtime.workloads.filter((item) => item?.sourceCommit === latest.sourceCommit)
  : [];
const runtimeReady = sourceWorkloads.length > 0 && sourceWorkloads.every((item) => Number.isInteger(item.readyReplicas)
  && Number.isInteger(item.replicas)
  && item.replicas > 0
  && item.readyReplicas >= item.replicas
  && item.updatedReplicas >= item.replicas
  && item.observedGeneration >= item.generation);
if (['pac-gitops-missing', 'pac-source-observation-inconsistent'].includes(output.code)
  && artifact.collector?.mode === 'taskrun-status-only'
  && latest.status === 'True'
  && runtimeReady) {
  output.ok = true;
  output.code = 'pac-artifact-log-evidence-deferred';
  output.phase = 'read-only-evidence-deferred';
  output.hint = 'PipelineRun and exact-source workloads are minimally ready; use id-specific history when synchronous TaskRun log evidence is required';
  output.warnings = [...(Array.isArray(output.warnings) ? output.warnings : []), {
    code: 'pac-artifact-log-evidence-deferred',
    blocking: false,
    pipelineRun: latest.name || null,
    valuesPrinted: false,
  }];
}
if (process.env.UNIDESK_PAC_DELIVERY_PROVENANCE_REQUIRED === '1' && admissionProvenance.ready !== true) {
  output.warnings = [...(Array.isArray(output.warnings) ? output.warnings : []), {
      code: 'pac-admission-provenance-not-ready',
      blocking: false,
      hint: 'PaC admission policy, binding, desired RBAC, or live default-SA observation is drifted; delivery continues',
      reasons: Array.isArray(admissionProvenance.reasons) ? admissionProvenance.reasons : [],
    }];
}
if (consumerBootstrap.configured === true && consumerBootstrap.ready !== true) {
  output.ok = false;
  output.code = 'pac-consumer-bootstrap-not-ready';
  output.phase = 'consumer-bootstrap-not-ready';
  output.hint = 'declared runner ServiceAccount/RoleBinding or Argo repository credential is missing or drifted';
}
process.stdout.write(JSON.stringify(output));
NODE
)
  status_progress complete
  printf '{"ok":%s,"crdPresent":%s,"controllerReady":"%s","admissionProvenance":%s,"consumerBootstrap":%s,"repository":{"name":"%s","repo":"%s/%s","url":"%s","condition":"%s"},"repositoryCondition":"%s","webhooks":%s,"pipelineRuns":%s,"taskRuns":%s,"artifact":%s,"argo":%s,"runtime":%s,"diagnostics":%s,"valuesPrinted":false}\n' \
    "$( [ -n "$crd" ] && [ "$controller_ready" != "0/0" ] && [ "$bootstrap_ready" = "true" ] && echo true || echo false )" \
    "$( [ -n "$crd" ] && echo true || echo false )" \
    "$(json_string "$controller_ready")" \
    "$UNIDESK_PAC_ADMISSION_STATE_JSON" \
    "$consumer_bootstrap" \
    "$(json_string "$UNIDESK_PAC_REPOSITORY_NAME")" \
    "$(json_string "$UNIDESK_PAC_GITEA_OWNER")" \
    "$(json_string "$UNIDESK_PAC_GITEA_REPO")" \
    "$(json_string "$UNIDESK_PAC_REPOSITORY_URL")" \
    "$(json_string "$repository_condition")" \
    "$(json_string "$repository_condition")" \
    "$hooks" "$pipelines" "$tasks" "$artifact" "$argo" "$runtime" "$diagnostics"
}

history_action() {
  crd=$(kubectl get crd repositories.pipelinesascode.tekton.dev -o name 2>/dev/null || true)
  controller_ready=$(kubectl -n "$UNIDESK_PAC_RELEASE_NAMESPACE" get deploy "$UNIDESK_PAC_CONTROLLER_SERVICE_NAME" -o jsonpath='{.status.readyReplicas}/{.spec.replicas}' 2>/dev/null || echo "0/0")
  repository_condition=$(condition_status "$UNIDESK_PAC_TARGET_NAMESPACE" repository "$UNIDESK_PAC_REPOSITORY_NAME")
  hooks=$(hook_summary)
  prepare_admission_provenance_state
  history=$(history_rows)
  rows=$(printf '%s' "$history" | node -e 'const fs=require("fs"); const h=JSON.parse(fs.readFileSync(0,"utf8")||"{}"); process.stdout.write(JSON.stringify(h.rows||[]));')
  consumers=$(printf '%s' "$history" | node -e 'const fs=require("fs"); const h=JSON.parse(fs.readFileSync(0,"utf8")||"{}"); process.stdout.write(JSON.stringify(h.consumers||[]));')
  errors=$(printf '%s' "$history" | node -e 'const fs=require("fs"); const h=JSON.parse(fs.readFileSync(0,"utf8")||"{}"); process.stdout.write(JSON.stringify(h.errors||[]));')
  printf '{"ok":%s,"crdPresent":%s,"controllerReady":"%s","admissionProvenance":%s,"repositoryCondition":"%s","repository":{"name":"%s","repo":"%s/%s","url":"%s"},"consumer":"%s","display":{"timeZone":"%s"},"consumerRows":%s,"rows":%s,"historyErrors":%s,"webhooks":%s,"source":"gitea-pac-tekton-live","historyStore":"none","valuesPrinted":false}\n' \
    "$( [ -n "$crd" ] && [ "$controller_ready" != "0/0" ] && echo true || echo false )" \
    "$( [ -n "$crd" ] && echo true || echo false )" \
    "$(json_string "$controller_ready")" \
    "$UNIDESK_PAC_ADMISSION_STATE_JSON" \
    "$(json_string "$repository_condition")" \
    "$(json_string "$UNIDESK_PAC_REPOSITORY_NAME")" \
    "$(json_string "$UNIDESK_PAC_GITEA_OWNER")" \
    "$(json_string "$UNIDESK_PAC_GITEA_REPO")" \
    "$(json_string "$UNIDESK_PAC_REPOSITORY_URL")" \
    "$(json_string "$UNIDESK_PAC_CONSUMER_ID")" \
    "$(json_string "$UNIDESK_PAC_DISPLAY_TIME_ZONE")" \
    "$consumers" "$rows" "$errors" "$hooks"
}

diagnose_regression_action() {
  history=$(regression_rows)
  rows=$(printf '%s' "$history" | node -e 'const fs=require("fs"); const h=JSON.parse(fs.readFileSync(0,"utf8")||"{}"); process.stdout.write(JSON.stringify(h.rows||[]));')
  consumers=$(printf '%s' "$history" | node -e 'const fs=require("fs"); const h=JSON.parse(fs.readFileSync(0,"utf8")||"{}"); process.stdout.write(JSON.stringify(h.consumers||[]));')
  errors=$(printf '%s' "$history" | node -e 'const fs=require("fs"); const h=JSON.parse(fs.readFileSync(0,"utf8")||"{}"); process.stdout.write(JSON.stringify(h.errors||[]));')
  printf '{"ok":%s,"consumer":"%s","display":{"timeZone":"%s"},"consumerRows":%s,"rows":%s,"historyErrors":%s,"webhooks":[],"source":"gitea-pac-pipelinerun-summary","historyStore":"none","valuesPrinted":false}\n' \
    "$( [ "$errors" = "[]" ] && echo true || echo false )" \
    "$(json_string "$UNIDESK_PAC_CONSUMER_ID")" \
    "$(json_string "$UNIDESK_PAC_DISPLAY_TIME_ZONE")" \
    "$consumers" "$rows" "$errors"
}

debug_step_action() {
  fixtures=$(node <<'NODE'
const { runPacStatusFixtureChecks } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
const result = runPacStatusFixtureChecks();
process.stdout.write(JSON.stringify(result));
NODE
)
  if [ -z "$UNIDESK_PAC_HISTORY_ID" ]; then
    printf '%s\n' "$fixtures"
    FIXTURES_JSON="$fixtures" node -e 'const result=JSON.parse(process.env.FIXTURES_JSON||"{}"); if(!result.ok) process.exitCode=1'
    return
  fi
  prepare_admission_provenance_state
  history=$(history_rows)
  UNIDESK_PAC_DEBUG_FIXTURES_JSON="$fixtures" UNIDESK_PAC_DEBUG_HISTORY_JSON="$history" node <<'NODE'
function record(value) {
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
}
function terminalRow(sourceObservation, key, step) {
  const source = record(record(sourceObservation.terminalSources)[key]);
  return {
    step,
    present: source.present === true,
    status: source.status || 'missing',
    source: source.source || null,
    taskRun: source.taskRun || null,
    reason: source.reason || null,
    results: Array.isArray(source.results) ? source.results : [],
    markerPresent: source.markerPresent === true,
    markerSource: source.markerSource || null,
    valuesPrinted: false,
  };
}
const fixtures = JSON.parse(process.env.UNIDESK_PAC_DEBUG_FIXTURES_JSON || '{}');
const history = JSON.parse(process.env.UNIDESK_PAC_DEBUG_HISTORY_JSON || '{}');
const rows = Array.isArray(history.rows) ? history.rows : [];
const historyErrors = Array.isArray(history.errors) ? history.errors : [];
const row = record(rows[0]);
const cp = require('node:child_process');
const sourceObservation = record(row.sourceObservation);
const artifact = record(row.artifact);
const catalog = record(artifact.catalog);
const collector = record(row.collector);
const terminal = [
  terminalRow(sourceObservation, 'planArtifacts', 'plan-artifacts'),
  terminalRow(sourceObservation, 'collectArtifacts', 'collect-artifacts'),
  terminalRow(sourceObservation, 'gitopsPromote', 'gitops-promote'),
];
function safeLogLine(value) {
  return String(value || '')
    .replace(/(authorization\s*[:=]\s*)(?:bearer\s+)?\S+/giu, '$1[REDACTED]')
    .replace(/(token|password|secret|api[_-]?key)(\s*[:=]\s*)\S+/giu, '$1$2[REDACTED]')
    .replace(/\b(?:sk|ghp|github_pat)-[A-Za-z0-9_-]{8,}\b/gu, '[REDACTED]')
    .replace(/https?:\/\/[^\s/]+@/giu, 'https://[REDACTED]@')
    .replace(/([?&][^=\s]+)=([^&\s]+)/gu, '$1=[REDACTED]')
    .slice(0, 320);
}
function failedTaskEvidence() {
  const task = record(record(row.taskRuns).failed);
  if (typeof task.name !== 'string' || task.name.length === 0) return null;
  const result = cp.spawnSync('kubectl', ['-n', process.env.UNIDESK_PAC_TARGET_NAMESPACE, 'logs', '-l', `tekton.dev/taskRun=${task.name}`, '--all-containers=true', '--prefix=true', '--tail=40', '--max-log-requests=10'], {
    encoding: 'utf8', timeout: 8000, maxBuffer: 256 * 1024,
  });
  const lines = String(result.stdout || result.stderr || '').split(/\r?\n/u).filter(Boolean).slice(-12).map(safeLogLine);
  const step = record(task.failedStep);
  return {
    taskRun: task.name,
    taskStatus: task.status || null,
    taskReason: task.reason || null,
    step: step.name || null,
    container: step.container || null,
    exitCode: Number.isInteger(step.exitCode) ? step.exitCode : null,
    terminationReason: step.terminationReason || step.reason || null,
    logReadOk: !result.error && result.status === 0,
    logTail: lines,
    truncated: lines.length >= 12,
    valuesPrinted: false,
  };
}
const failureEvidence = failedTaskEvidence();
let firstBreak = null;
function breakAt(code, stage, reason) {
  if (firstBreak === null) firstBreak = { code, stage, reason, valuesPrinted: false };
}
if (historyErrors.length > 0) breakAt('target-read-failed', 'target-read', historyErrors[0]?.error || historyErrors[0]?.stderr || 'target-side history read failed');
if (Object.keys(row).length === 0) breakAt('pipeline-run-not-found', 'pipeline-run', `PipelineRun ${process.env.UNIDESK_PAC_HISTORY_ID || '-'} was not found for the selected consumer`);
if (Object.keys(row).length > 0 && row.deliveryAuthorityEligible !== true) breakAt('pipeline-run-not-delivery-authority', 'pipeline-run', `PipelineRun is classified as ${row.deliveryClass || 'unknown'}; only an outer PaC event can drive delivery closeout`);
if (Object.keys(row).length > 0 && Object.keys(record(row.firstBreak)).length > 0) {
  const typed = record(row.firstBreak);
  breakAt(typed.code || 'pipeline-run-not-succeeded', typed.phase || typed.stage || 'pipeline-run', typed.reason || `PipelineRun terminal status is ${row.status || row.reason || 'unknown'}`);
}
if (Object.keys(row).length > 0 && row.status !== 'True') breakAt('pipeline-run-not-succeeded', 'pipeline-run', `PipelineRun terminal status is ${row.status || row.reason || 'unknown'}`);
if (Object.keys(row).length > 0 && Object.keys(sourceObservation).length === 0) breakAt('source-observation-missing', 'plan-artifacts', 'delivery plan structured evidence is missing');
if (sourceObservation.sourceMatched !== true) breakAt('source-observation-mismatch', 'plan-artifacts', 'delivery plan source commit does not match the selected PipelineRun');
if (sourceObservation.valid !== true) breakAt('source-observation-invalid', 'plan-artifacts', sourceObservation.reason || 'delivery plan is incomplete or contradictory');
for (const item of terminal) {
  if (item.present !== true || item.status !== 'succeeded') breakAt('terminal-evidence-incomplete', item.step, `${item.step} TaskRun terminal status is ${item.status}`);
}
if (Number(collector.logErrorCount || 0) > 0) breakAt('contract-log-read-failed', 'collector', 'one or more contract TaskRun logs could not be read');
if (sourceObservation.mode === 'delivery') {
  if (catalog.present !== true || catalog.status !== 'published') breakAt('artifact-catalog-missing', 'collect-artifacts', 'published artifact catalog evidence is missing');
  if (catalog.registryVerified !== true) breakAt('artifact-catalog-unverified', 'collect-artifacts', 'artifact catalog registry verification is not true');
  if (!Array.isArray(artifact.digests) || artifact.digests.length === 0) breakAt('artifact-digest-missing', 'collect-artifacts', 'artifact catalog did not expose any sha256 digest');
  if (typeof artifact.gitopsCommit !== 'string' || artifact.gitopsCommit.length === 0) breakAt('gitops-commit-missing', 'gitops-promote', 'GitOps commit is not visible in the successful promotion TaskRun');
}
const realRun = {
  ok: firstBreak === null,
  found: Object.keys(row).length > 0,
  pipelineRun: Object.keys(row).length === 0 ? null : {
    id: row.id || row.pipelineRun || null,
    status: row.status || null,
    reason: row.reason || null,
    sourceCommit: row.commit || null,
    sourceMatched: sourceObservation.sourceMatched === true,
    deliveryClass: row.deliveryClass || 'unknown',
    deliveryAuthorityEligible: row.deliveryAuthorityEligible === true,
    deliveryOwner: row.deliveryOwner || null,
    executionOwner: row.executionOwner || null,
    parentRelation: row.parentRelation || null,
    valuesPrinted: false,
  },
  mode: sourceObservation.mode || null,
  valid: sourceObservation.valid === true,
  terminal,
  artifact: {
    imageStatus: artifact.imageStatus || null,
    envReuse: artifact.envReuse || null,
    digests: Array.isArray(artifact.digests) ? artifact.digests : [],
    gitopsCommit: artifact.gitopsCommit || null,
    sourceCommit: artifact.sourceCommit || null,
    catalog: {
      present: catalog.present === true,
      status: catalog.status || null,
      sourceCommit: catalog.sourceCommit || null,
      registryVerified: catalog.registryVerified === true,
      publishedCount: catalog.publishedCount ?? null,
      reusedCount: catalog.reusedCount ?? null,
      requiredServiceCount: catalog.requiredServiceCount ?? null,
      digestCount: catalog.digestCount ?? null,
      source: catalog.source || null,
      valuesPrinted: false,
    },
    valuesPrinted: false,
  },
  collector: {
    mode: collector.mode || null,
    matchingTaskCount: collector.matchingTaskCount ?? null,
    contractTaskCount: collector.contractTaskCount ?? null,
    terminalRecordCount: collector.terminalRecordCount ?? null,
    logErrorCount: collector.logErrorCount ?? null,
    logErrors: Array.isArray(collector.logErrors) ? collector.logErrors : [],
    valuesPrinted: false,
  },
  traceId: row.traceId || null,
  error: Object.keys(record(row.error)).length === 0 ? null : record(row.error),
  failureEvidence,
  firstBreak,
  valuesPrinted: false,
};
const out = {
  ok: fixtures.ok === true && realRun.ok,
  checks: Array.isArray(fixtures.checks) ? fixtures.checks : [],
  realRun,
  valuesPrinted: false,
};
process.stdout.write(`${JSON.stringify(out)}\n`);
if (!out.ok) process.exitCode = 1;
NODE
}

case "$UNIDESK_PAC_ACTION" in
  apply) apply_action ;;
  status) status_action ;;
  history) history_action ;;
  diagnose-regression) diagnose_regression_action ;;
  debug-step) debug_step_action ;;
  disable-automatic-webhooks) disable_automatic_webhooks_action ;;
  manual-release-plan|manual-release-trigger) manual_release_action ;;
  *) printf '{"ok":false,"error":"unsupported-action","valuesPrinted":false}\n'; exit 2 ;;
esac
