← Files NightshiftARCHIVED FILE

runtime/status.sh

7.92 KB · Oct 2, 2026 · 00:30 UTC

↓ Download file

#!/usr/bin/env bash
# status.sh — read-only shift status for the native Status skill.
#
#   status.sh --project DIR
#
# Prints a compact glanceable summary: punch-list progress, evidence counts, resolved policy,
# preflight gaps, and liveness vs checkpoint vs stall as separate lines. Never writes.
#
# Exit: 0 report printed · 1 usage
set -u

_here="${BASH_SOURCE[0]%/*}"
[ "$_here" != "${BASH_SOURCE[0]}" ] || _here=.
# shellcheck source=plugins/nightshift/lib/lib.sh
. "$_here/../lib/lib.sh"

PROJECT="${CLAUDE_PROJECT_DIR:-${CODEX_PROJECT_DIR:-$PWD}}"
while [ $# -gt 0 ]; do
  case "$1" in
    --project)
      [ $# -ge 2 ] || { printf 'status: --project needs a value\n' >&2; exit 1; }
      PROJECT="$2"
      shift 2
      ;;
    -h | --help)
      awk 'NR == 1 { next } !/^#/ { exit } { sub(/^# ?/, ""); print }' "$0"
      exit 1
      ;;
    *) printf 'status: unknown argument: %s\n' "$1" >&2; exit 1 ;;
  esac
done

HOST="$(cd -P "$PROJECT" 2>/dev/null && pwd)" || {
  printf 'status: cannot cd to %s\n' "$PROJECT" >&2
  exit 1
}

WORKSPACE="$HOST"
if [ -e "$HOST/.nightshift-link" ] || [ -L "$HOST/.nightshift-link" ]; then
  WORKSPACE="$(ns_workspace_root "$HOST" 2>/dev/null)" || WORKSPACE="$HOST"
fi
NS="$WORKSPACE/.nightshift"
declare PUNCH ARMED_FILE PARKING DRAFTING_TABLE WORK_ORDERS SNAGS MAP STOP_FILE SESSION_FILE WATCH_REASON LOG
ns_layout_set PUNCH "$NS" punch-list
ns_layout_set ARMED_FILE "$NS" armed
ns_layout_set PARKING "$NS" parking-lot
ns_layout_set DRAFTING_TABLE "$NS" drafting-table
ns_layout_set WORK_ORDERS "$NS" work-orders
ns_layout_set SNAGS "$NS" snag-log
ns_layout_set MAP "$NS" opportunity-map
ns_layout_set STOP_FILE "$NS" stop
ns_layout_set SESSION_FILE "$NS" session
ns_layout_set WATCH_REASON "$NS" watch-reason
ns_layout_set WATCHMAN_FILE "$NS" watchman
ns_layout_set LOG "$NS" shift-log

emit() { printf '%s\n' "$1"; }
# A fact whose value is empty is still a fact: `none` is an answer, a blank line is not.
fact() { printf '%s %s\n' "$1" "${2:-none}"; }

if [ ! -d "$NS" ]; then
  emit "Nightshift Status"
  emit "Nightshift: missing at $WORKSPACE"
  exit 0
fi

OPEN=0
TICKED=0
[ -f "$PUNCH" ] && OPEN="$(ns_open_boxes "$PUNCH")" && TICKED="$(ns_ticked_boxes "$PUNCH")"
ARMED=0
[ -f "$ARMED_FILE" ] && ARMED=1
WATCH="$(rule "$WORKSPACE" watchMinutes "${NIGHTSHIFT_WATCH:-}")"
case "$WATCH" in '' | *[!0-9]*) WATCH=0 ;; esac

STOP_REASON="$(ns_status_stop_reason "$NS" 2>/dev/null)"

emit "Nightshift Status"
emit "Workspace:   $WORKSPACE"
emit "Shift:       $([ "$ARMED" -eq 1 ] && printf armed || printf 'not armed')"
emit "Items:       open=$OPEN ticked=$TICKED"
emit "evidence:    $(ns_evidence_counts "$WORKSPACE")"
emit "liveness:    $(ns_status_liveness "$NS" "$WATCH")"
activity="$(ns_status_last_activity "$NS")"
emit "last activity: ${activity:-none}"
emit "last checkpoint: $(ns_status_last_checkpoint "$WORKSPACE")"
emit "stall attempts: $(ns_status_stall_attempts "$NS")"

# The facts, derived here rather than by hand in the skill. One per line, stable label first, so
# the model renders them rather than recomputing them.
emit ""
emit "facts"
fact "schema" "$(ns_state_version "$WORKSPACE" 2>/dev/null)"

# Unarmed with work still open is the one state that reads wrong at a glance: a punch list nobody
# is holding is a to-do file, and only Start makes it a shift.
if [ "$ARMED" -eq 0 ] && [ "${OPEN:-0}" -gt 0 ]; then
  fact "armed" "no (the punch list is a to-do file, not a shift; Start begins one)"
else
  fact "armed" "$([ "$ARMED" -eq 1 ] && printf yes || printf no)"
fi

fact "open item" "$(ns_status_open_title "$PUNCH")"
fact "parked" "$(ns_status_entry_count "$PARKING")"
ns_status_entry_titles "$PARKING" 0 | while IFS= read -r line; do
  [ -n "$line" ] && fact "parked entry" "$line"
done

DRAFTS="$(ns_open_drafts "$DRAFTING_TABLE" 2>/dev/null)" || DRAFTS=0
ORDERS="$(ns_open_boxes_file "$WORK_ORDERS" 2>/dev/null)" || ORDERS=0
# With approved work open, staged work is informational and nothing else: Start works the punch
# list exactly as the owner left it.
if [ "${OPEN:-0}" -gt 0 ]; then
  fact "staged" "drafts=$DRAFTS orders=$ORDERS (informational while items are open)"
else
  fact "staged" "drafts=$DRAFTS orders=$ORDERS"
fi

ns_status_entry_titles "$SNAGS" 3 | while IFS= read -r line; do
  [ -n "$line" ] && fact "snag" "$line"
done

fact "opportunities" "$(ns_status_opportunity_counts "$MAP")"
ns_status_building "$MAP" | while IFS="$(printf '\t')" read -r key value; do
  [ -n "$key" ] && fact "building $key" "$value"
done

DEADLINE="$(ns_status_deadline_remaining "$NS" 2>/dev/null)"
fact "deadline" "${DEADLINE:-none (finite list)}"
if [ -f "$STOP_FILE" ]; then
  fact "stop" "present${STOP_REASON:+ ($STOP_REASON)}"
else
  fact "stop" "absent"
fi
fact "session" "$([ -f "$SESSION_FILE" ] && printf 'bound' || printf 'none')"
fact "lease" "$(ns_lease_valid "$NS" >/dev/null 2>&1 && printf 'held' || printf 'absent or unowned')"

# Whether anything is watching the shift. An armed shift with work left and no live watchman is
# not revived after a crash or a usage limit; only watchMinutes 0 means that on purpose.
WATCHMAN_STATE=none
if [ -L "$WATCHMAN_FILE" ]; then
  WATCHMAN_STATE="not a usable file"
elif [ -f "$WATCHMAN_FILE" ]; then
  WPID="$(sed -n 1p "$WATCHMAN_FILE" 2>/dev/null | tr -d '[:space:]')"
  case "$WPID" in
    '' | *[!0-9]*) ;;
    *) if kill -0 "$WPID" 2>/dev/null; then WATCHMAN_STATE="alive (pid $WPID)"; else WATCHMAN_STATE="stale (pid $WPID)"; fi ;;
  esac
fi
# A reason is the last thing a watchman recorded; once that watchman is gone it describes the past.
REASON_NOTE=""
case "$WATCHMAN_STATE" in alive*) ;; *) REASON_NOTE="; the watchman that recorded it is not running" ;; esac
if [ -f "$WATCH_REASON" ]; then
  REASON_CODE="$(ns_reason_code "$NS" 2>/dev/null)"
  fact "watch reason" "${REASON_CODE:-none}${REASON_CODE:+ ($(ns_reason_label "$REASON_CODE" 2>/dev/null)$REASON_NOTE)}"
else
  fact "watch reason" "none"
fi
fact "watchman" "$WATCHMAN_STATE"
case "$WATCHMAN_STATE" in
  alive* | "not a usable file") ;;
  *)
    if [ "$ARMED" -eq 1 ] && [ "$OPEN" -gt 0 ] && [ "$(rule "$WORKSPACE" watchMinutes "${NIGHTSHIFT_WATCH:-}")" != 0 ]; then
      fact "watchman warning" "the shift is armed with open items and nothing is watching it; a crash or usage limit will not be revived until ns start-watchman arms one again"
    fi
    ;;
esac


fact "work mode" "$(ns_work_mode "$WORKSPACE" 2>/dev/null)"
fact "work target" "$(ns_work_target "$WORKSPACE" 2>/dev/null)"
RECEIPTS="$(ns_receipts_count "$WORKSPACE" 2>/dev/null)" || RECEIPTS=0
fact "artifact receipts" "$RECEIPTS"
fact "latest artifact receipt" "$(ns_latest_receipt "$WORKSPACE" 2>/dev/null)"
UNUSABLE_RECV=0
if [ "$(ns_work_mode "$WORKSPACE" 2>/dev/null)" = artifact ]; then
  RECV_PATH="$(ns_receipts_dir "$WORKSPACE" 2>/dev/null)"
  if { [ -e "$RECV_PATH" ] || [ -L "$RECV_PATH" ]; } &&
    ! ns_receipts_usable_dir "$WORKSPACE" >/dev/null 2>&1; then
    UNUSABLE_RECV=1
    fact "receipts warning" "the artifact receipts path is not a usable directory"
  fi
fi
if ns_receipts_enabled "$WORKSPACE"; then
  fact "completion record" "per-item receipt"
  # A planted file where receipts/ belongs is reported as itself; do not also
  # count missing receipt text for that path.
  if [ "$UNUSABLE_RECV" -eq 0 ]; then
    MISSING="$(ns_receipts_missing_count "$WORKSPACE" 2>/dev/null)" || MISSING=0
    if [ "${MISSING:-0}" -gt 0 ]; then
      fact "receipts missing model text" "$MISSING"
    fi
  fi
else
  fact "completion record" "none; the owner disabled receipts"
fi

ns_status_transitions "$LOG" 3 | while IFS= read -r line; do
  [ -n "$line" ] && fact "transition" "$line"
done

emit ""
emit "resolved policy"
if POLICY_LINES="$(ns_policy_resolve_table "$WORKSPACE" 2>/dev/null)"; then
  printf '%s\n' "$POLICY_LINES"
else
  emit "none"
fi
emit ""
emit "preflight gaps"
PREFLIGHT=""
if ns_policy_json_tool >/dev/null 2>&1; then
  PREFLIGHT="$("$_here/preflight-needs.sh" --project "$WORKSPACE" 2>/dev/null)" || PREFLIGHT=""
fi
if [ -n "$PREFLIGHT" ]; then
  printf '%s\n' "$PREFLIGHT"
else
  emit "none"
fi
exit 0

SHA-256: 910f596b599f54bb0d4ade3f0b11690a93ac8d6d0452f8328e81645c96e8cfbe