@indigoai-us/hq-cli 5.345.109 → 5.345.110

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.109",
3
+ "version": "5.345.110",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -48,7 +48,7 @@
48
48
  "@aws-sdk/client-iot-data-plane": "^3.1096.0",
49
49
  "@aws-sdk/client-s3": "^3.1049.0",
50
50
  "@aws-sdk/dsql-signer": "^3.1146.0",
51
- "@indigoai-us/hq-cloud": "~6.18.67",
51
+ "@indigoai-us/hq-cloud": "~6.18.69",
52
52
  "@indigoai-us/hq-flags-client": "^0.1.2",
53
53
  "@modelcontextprotocol/sdk": "1.29.0",
54
54
  "@sentry/node": "^10.49.0",
@@ -1,199 +0,0 @@
1
- #!/usr/bin/env bash
2
- # hq-core: public
3
- # policy-age-report.sh — measure policy usage and flag retirement candidates
4
- # (2026-09-07).
5
- #
6
- # Why: nothing retired a policy. About 85% of one install's 1,251 personal
7
- # policies were machine-written, and the only "usage" signal was that the
8
- # loader printed them — which, above the host's output ceiling, meant nothing.
9
- # This report gives the fade-out its inputs. It does NOT delete anything; it
10
- # names candidates with the evidence, for a human (or /learn --retire) to act on.
11
- #
12
- # Signals per policy (from workspace/orchestrator/policy-trigger-state/*.txt,
13
- # one ledger per session, and the policy files themselves):
14
- # fired number of sessions whose ledger recorded the slug (emitted)
15
- # retrieved number of sessions that pulled the full text (record-policy-retrieval.sh)
16
- # last_fired newest ledger mtime that contains the slug (UTC date)
17
- # age_days days since frontmatter `created:` (or file mtime)
18
- # stale_refs backtick-quoted HQ-relative paths in the Rule text that no
19
- # longer exist on disk (an inaccuracy proxy). Only core/,
20
- # .claude/, companies/<co>/ paths are judged; lines that talk
21
- # about a path being deleted/removed/legacy are not dependencies;
22
- # a same-stem sibling (foo.sh -> foo.mjs) counts as renamed.
23
- # single_incident frontmatter source is a one-off (user-correction,
24
- # session-learning, back-pressure-failure) AND version == 1
25
- #
26
- # Candidate classes (--candidates):
27
- # never-fired fired == 0 and age_days >= --min-age (default 30)
28
- # dormant fired > 0 but last_fired older than --dormant-days (default 90)
29
- # stale-refs stale_refs > 0 (rule points at something that is gone)
30
- # single-incident-old single_incident, age_days >= --incident-days (default 60), and fired <= 1
31
- #
32
- # Usage:
33
- # policy-age-report.sh [--dir <policies-dir>]... [--json] [--candidates] [--min-age N] [--dormant-days N] [--incident-days N]
34
- # Defaults: personal/policies and core/policies (add --dir companies/<co>/policies when bound).
35
- set -uo pipefail
36
- HQ_ROOT="${HQ_ROOT:-${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}}"
37
- LEDGERS="${HQ_POLICY_LEDGER_DIR:-$HQ_ROOT/workspace/orchestrator/policy-trigger-state}"
38
- DIRS=(); JSON=0; CAND=0; MIN_AGE=30; DORMANT=90; INCIDENT=60
39
- while [ $# -gt 0 ]; do
40
- case "$1" in
41
- --dir) DIRS+=("$2"); shift 2 ;;
42
- --json) JSON=1; shift ;;
43
- --candidates) CAND=1; shift ;;
44
- --min-age) MIN_AGE="$2"; shift 2 ;;
45
- --dormant-days) DORMANT="$2"; shift 2 ;;
46
- --incident-days) INCIDENT="$2"; shift 2 ;;
47
- -h|--help) sed -n 2,32p "$0"; exit 0 ;;
48
- *) echo "policy-age-report: unknown arg $1" >&2; exit 2 ;;
49
- esac
50
- done
51
- [ "${#DIRS[@]}" -gt 0 ] || DIRS=("$HQ_ROOT/personal/policies" "$HQ_ROOT/core/policies")
52
- command -v jq >/dev/null 2>&1 || { echo "policy-age-report: jq required" >&2; exit 2; }
53
- NOW_EPOCH="${HQ_POLICY_REPORT_NOW_EPOCH:-$(date +%s)}"
54
- if [ -n "${HQ_POLICY_REPORT_NOW_EPOCH:-}" ]; then
55
- CLOCK_INPUT="$HQ_POLICY_REPORT_NOW_EPOCH"
56
- if [[ ! "$CLOCK_INPUT" =~ ^[+-]?[0-9]+$ ]]; then
57
- printf 'policy-age-report: HQ_POLICY_REPORT_NOW_EPOCH must be an integer: %s\n' "$CLOCK_INPUT" >&2
58
- exit 1
59
- fi
60
- CLOCK_NEGATIVE=0
61
- CLOCK_DIGITS="$CLOCK_INPUT"
62
- case "$CLOCK_DIGITS" in
63
- -*) CLOCK_NEGATIVE=1; CLOCK_DIGITS="${CLOCK_DIGITS#-}" ;;
64
- +*) CLOCK_DIGITS="${CLOCK_DIGITS#+}" ;;
65
- esac
66
- while [[ "${#CLOCK_DIGITS}" -gt 1 && "${CLOCK_DIGITS:0:1}" = 0 ]]; do
67
- CLOCK_DIGITS="${CLOCK_DIGITS:1}"
68
- done
69
- CLOCK_LIMIT=9223372036854775807
70
- [ "$CLOCK_NEGATIVE" = 1 ] && CLOCK_LIMIT=9223372036854775808
71
- if [[ "${#CLOCK_DIGITS}" -gt "${#CLOCK_LIMIT}" ]] || { [[ "${#CLOCK_DIGITS}" -eq "${#CLOCK_LIMIT}" ]] && (LC_ALL=C; [[ "$CLOCK_DIGITS" > "$CLOCK_LIMIT" ]]); }; then
72
- printf 'policy-age-report: HQ_POLICY_REPORT_NOW_EPOCH must be an integer: %s\n' "$CLOCK_INPUT" >&2
73
- exit 1
74
- fi
75
- if [ "$CLOCK_NEGATIVE" = 1 ]; then
76
- NOW_EPOCH=$((0 - 10#$CLOCK_DIGITS))
77
- else
78
- NOW_EPOCH=$((10#$CLOCK_DIGITS))
79
- fi
80
- fi
81
- TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
82
-
83
- # One pass over every ledger: slug<TAB>mtime, then reduce to count + max mtime.
84
- HAVE_LEDGERS=0
85
- if [ -d "$LEDGERS" ] && [ -n "$(find "$LEDGERS" -name '*.txt' -type f 2>/dev/null | head -1)" ]; then
86
- HAVE_LEDGERS=1
87
- find "$LEDGERS" -name '*.txt' -type f 2>/dev/null | while IFS= read -r f; do
88
- m="$(stat -c %Y "$f" 2>/dev/null || stat -f %m "$f")"
89
- awk -v m="$m" 'NF { print $1 "\t" m }' "$f"
90
- done | awk -F'\t' '{ c[$1]++; if ($2 > mx[$1]) mx[$1] = $2 } END { for (s in c) print s "\t" c[s] "\t" mx[s] }' > "$TMP/usage.tsv"
91
- else
92
- : > "$TMP/usage.tsv"
93
- fi
94
- # Retrieval ledger (record-policy-retrieval.sh): sessions that PULLED the full text.
95
- RETRIEVALS="${HQ_POLICY_RETRIEVAL_LEDGER_DIR:-$HQ_ROOT/workspace/orchestrator/policy-retrieval-state}"
96
- if [ -d "$RETRIEVALS" ]; then
97
- cat "$RETRIEVALS"/*.txt 2>/dev/null | awk 'NF { c[$1]++ } END { for (s in c) print s "\t" c[s] }' > "$TMP/retrieved.tsv"
98
- else
99
- : > "$TMP/retrieved.tsv"
100
- fi
101
-
102
- to_epoch() { # YYYY-MM-DD -> epoch (0 on failure)
103
- local epoch normalized
104
- epoch="$(date -u -d "$1 00:00:00" +%s 2>/dev/null || date -u -j -f '%Y-%m-%d %H:%M:%S' "$1 00:00:00" +%s 2>/dev/null)" || { echo 0; return; }
105
- [ -n "$epoch" ] || { echo 0; return; }
106
- normalized="$(date -u -d "@$epoch" +%Y-%m-%d 2>/dev/null || date -u -r "$epoch" +%Y-%m-%d 2>/dev/null)" || { echo 0; return; }
107
- [ "$normalized" = "$1" ] || { echo 0; return; }
108
- echo "$epoch"
109
- }
110
-
111
- for dir in "${DIRS[@]}"; do
112
- case "$dir" in /*) ;; *) dir="$HQ_ROOT/$dir" ;; esac
113
- [ -d "$dir" ] || continue
114
- for f in "$dir"/*.md; do
115
- [ -f "$f" ] || continue
116
- b="$(basename "$f")"
117
- case "$b" in README.md|example-policy.md|_digest*.md|*" "*|*.conflict-*|*.sync-conflict-*) continue ;; esac
118
- [ -L "$f" ] && continue # personal overlay mirrors live in core/ as symlinks; count the source once
119
- grep -q '^status: retired' "$f" && continue # already retired (policy-retire.sh)
120
- id="$(awk 'NR==1&&!/^---/{exit} /^---/{d++; if(d==2)exit; next} d==1&&/^id:/{sub(/^id:[ \t]*/,""); gsub(/^["'"'"']|["'"'"']$/,""); print; exit}' "$f")"
121
- [ -n "$id" ] || id="${b%.md}"
122
- enf="$(awk '/^---/{d++; if(d==2)exit; next} d==1&&/^enforcement:/{sub(/^enforcement:[ \t]*/,""); print; exit}' "$f")"
123
- src="$(awk '/^---/{d++; if(d==2)exit; next} d==1&&/^source:/{sub(/^source:[ \t]*/,""); gsub(/"/,""); print; exit}' "$f")"
124
- ver="$(awk '/^---/{d++; if(d==2)exit; next} d==1&&/^version:/{sub(/^version:[ \t]*/,""); print; exit}' "$f")"
125
- created="$(awk '/^---/{d++; if(d==2)exit; next} d==1&&/^created:/{sub(/^created:[ \t]*/,""); gsub(/"/,""); print substr($0,1,10); exit}' "$f")"
126
- if [ -n "$created" ]; then ce="$(to_epoch "$created")"; else ce=0; fi
127
- [ "$ce" -gt 0 ] || ce="$(stat -c %Y "$f" 2>/dev/null || stat -f %m "$f")"
128
- age_days=$(( (NOW_EPOCH - ce) / 86400 ))
129
- fired=0; last=""
130
- row="$(awk -F'\t' -v s="$id" '$1==s{print; exit}' "$TMP/usage.tsv")"
131
- if [ -n "$row" ]; then fired="$(cut -f2 <<<"$row")"; last_e="$(cut -f3 <<<"$row")"; last="$(date -u -d "@$last_e" +%Y-%m-%d 2>/dev/null || date -u -r "$last_e" +%Y-%m-%d)"; last_age=$(( (NOW_EPOCH - last_e) / 86400 )); else last_age=-1; fi
132
- retrieved="$(awk -F'\t' -v s="$id" '$1==s{print $2; exit}' "$TMP/retrieved.tsv")"; retrieved="${retrieved:-0}"
133
- # stale refs: backtick paths in the Rule section that look HQ-relative and are missing
134
- stale=""; renamed=""
135
- while IFS= read -r ref; do
136
- [ -n "$ref" ] || continue
137
- # Only release-owned or company-owned paths can make a rule inaccurate:
138
- # core/, .claude/, companies/<slug>/ (when that company exists here).
139
- # personal/, workspace/, repos/ are user- or environment-specific and are
140
- # never judged. A ref whose top-level dir is absent is not judged either.
141
- case "$ref" in
142
- .claude/state/*|.claude/worktrees/*|core/packages/*) continue ;; # runtime / install-specific
143
- core/*|.claude/*) ;;
144
- companies/*) co_ref="${ref#companies/}"; co_ref="${co_ref%%/*}"; [ -d "$HQ_ROOT/companies/$co_ref" ] || continue ;;
145
- *) continue ;;
146
- esac
147
- # Placeholder examples in prose (`core/scripts/foo.sh`, `.claude/hooks/example.sh`) are not dependencies.
148
- case "$(basename "$ref")" in foo*|bar*|baz*|example*|placeholder*|sample*|your-*|my-*|x.sh|dummy*) continue ;; esac
149
- if [ ! -e "$HQ_ROOT/$ref" ]; then
150
- # A sibling with the same stem (renamed extension: foo.sh -> foo.mjs) means
151
- # the subject still exists and the rule needs a fix, not retirement.
152
- ref_for_stem="${ref%/}"
153
- ref_basename="${ref_for_stem##*/}"
154
- case "$ref_basename" in
155
- .* )
156
- case "${ref_basename#.}" in
157
- *.*) stem="${ref_for_stem%.*}" ;;
158
- *) stem="$ref_for_stem" ;;
159
- esac
160
- ;;
161
- *.*) stem="${ref_for_stem%.*}" ;;
162
- *) stem="$ref_for_stem" ;;
163
- esac
164
- if ls "$HQ_ROOT/$stem".* >/dev/null 2>&1; then renamed="${renamed:+$renamed,}$ref"; else stale="${stale:+$stale,}$ref"; fi
165
- fi
166
- done < <(awk '/^## Rule/{r=1;next} r&&/^## /{r=0} r' "$f" \
167
- | grep -viE '(deleted|removed|retired|deprecated|no longer|not exist|never (call|use|run|read)|do not (call|use|run|read)|must not|instead of|was renamed|old path|legacy)' \
168
- | grep -oE '`(core|personal|companies|repos|workspace|\.claude)/[A-Za-z0-9_./-]+`' | tr -d '`' | grep -vE '\{|\}|\*' | sort -u)
169
- stale_n=0; [ -n "$stale" ] && stale_n="$(tr ',' '\n' <<<"$stale" | wc -l | tr -d ' ')"
170
- judged_n="$(awk '/^## Rule/{r=1;next} r&&/^## /{r=0} r' "$f" | grep -viE '(deleted|removed|retired|deprecated|no longer|not exist|never (call|use|run|read)|do not (call|use|run|read)|must not|instead of|was renamed|old path|legacy)' | grep -oE '`(core|\.claude|companies)/[A-Za-z0-9_./-]+`' | tr -d '`' | grep -vE '\{|\}|\*|/(foo|bar|baz|example|placeholder|sample|your-|my-|dummy)|^\.claude/state/|^\.claude/worktrees/|^core/packages/' | sort -u | wc -l | tr -d ' ')"
171
- single=0; case "$src" in user-correction|session-learning|back-pressure-failure) [ "${ver:-1}" = "1" ] && single=1 ;; esac
172
- classes=""
173
- if [ "$fired" = 0 ] && [ "$age_days" -ge "$MIN_AGE" ] && [ "$HAVE_LEDGERS" = 1 ]; then classes="never-fired"; fi
174
- if [ "$fired" -gt 0 ] && [ "$last_age" -ge "$DORMANT" ]; then classes="${classes:+$classes,}dormant"; fi
175
- if [ "$stale_n" -gt 0 ]; then classes="${classes:+$classes,}stale-refs"; fi
176
- if [ -n "$renamed" ]; then classes="${classes:+$classes,}renamed-refs"; fi
177
- # A one-off rule that keeps firing has earned its place; only flag it when it never (or once) fired.
178
- if [ "$single" = 1 ] && [ "$age_days" -ge "$INCIDENT" ] && [ "$fired" -le 1 ]; then classes="${classes:+$classes,}single-incident-old"; fi
179
- printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$id" "${f#"$HQ_ROOT"/}" "${enf:-unset}" "$age_days" "$fired" "${last:--}" "$stale_n" "$stale" "$single" "$classes" "$retrieved" "$judged_n" "$renamed"
180
- done
181
- done > "$TMP/rows.tsv"
182
-
183
- if [ "$JSON" = 1 ]; then
184
- jq -R -s -c --argjson cand "$CAND" '
185
- split("\n") | map(select(length>0) | split("\t") | {id:.[0], path:.[1], enforcement:.[2], age_days:(.[3]|tonumber), fired:(.[4]|tonumber), last_fired:(if .[5]=="-" then null else .[5] end), stale_refs:(if .[7]=="" then [] else (.[7]|split(",")) end), single_incident:(.[8]=="1"), candidate_classes:(if .[9]=="" then [] else (.[9]|split(",")) end), retrieved:((.[10] // "0")|tonumber), refs_judged:((.[11] // "0")|tonumber), renamed_refs:(if (.[12] // "")=="" then [] else (.[12]|split(",")) end)})
186
- | if $cand == 1 then map(select(.candidate_classes|length>0)) else . end' "$TMP/rows.tsv" | tr -d '\r'
187
- exit 0
188
- fi
189
- total="$(wc -l < "$TMP/rows.tsv" | tr -d ' ')"
190
- never="$(awk -F'\t' '$5==0' "$TMP/rows.tsv" | wc -l | tr -d ' ')"
191
- cands="$(awk -F'\t' '$10!=""' "$TMP/rows.tsv" | wc -l | tr -d ' ')"
192
- stale_total="$(awk -F'\t' '$7>0' "$TMP/rows.tsv" | wc -l | tr -d ' ')"
193
- echo "policy-age-report: $total policies, $never never fired, $stale_total with stale references, $cands retirement candidates (min-age ${MIN_AGE}d, dormant ${DORMANT}d, incident ${INCIDENT}d)"
194
- if [ "$CAND" = 1 ]; then
195
- awk -F'\t' '$10!="" { printf " %-14s %-60s fired=%-4s last=%-10s age=%-4sd %s%s\n", $10, $1, $5, $6, $4, ($7>0 ? "stale:" $8 : ""), "" }' "$TMP/rows.tsv" | sort
196
- else
197
- echo " (add --candidates to list them, --json for machine-readable rows)"
198
- fi
199
- exit 0
@@ -1,199 +0,0 @@
1
- #!/usr/bin/env bash
2
- # hq-core: public
3
- # policy-retire.sh — retire (or restore) a policy without deleting it (2026-09-07).
4
- #
5
- # Why: nothing retired policies, so the corpus only grew. Retirement is a
6
- # frontmatter state, not a deletion: the file stays where it is (history,
7
- # grep-ability, restore), and every consumer skips `status: retired` — the
8
- # trigger loader, the company-bind digest, the age report.
9
- #
10
- # Usage:
11
- # policy-retire.sh <slug-or-path> --reason "<why>" [--by <who>]
12
- # policy-retire.sh <slug-or-path> --restore
13
- # policy-retire.sh --from-report <age-report.json> --class <never-fired|dormant|stale-refs|single-incident-old> --reason "<why>" [--yes]
14
- # policy-retire.sh --auto [--dir <policies-dir>]... [--classes a,b] [--report-dir <dir>] [--dry-run]
15
- #
16
- # --auto is the human-ON-the-loop mode (2026-09-07): it runs the age report,
17
- # retires every candidate with a per-policy reason, and writes a dated report
18
- # (default workspace/reports/policy-retirement-<date>.md) so a wrong call can
19
- # be found and reversed with --restore. Nobody approves in advance. Classes and
20
- # reasons:
21
- # never-fired never emitted in any session ledger, older than 30 days
22
- # dormant last emitted more than 90 days ago
23
- # stale-refs its Rule text points at HQ paths that no longer exist
24
- # single-incident-old a one-off correction, older than 60 days, fired at most once
25
- # Defaults to personal/policies plus the bound company's policies; core/ is
26
- # release-owned and is retired only in the hq-core-staging tree.
27
- #
28
- # <slug-or-path>: a policy file path, or a slug looked up under personal/policies,
29
- # core/policies, and companies/*/policies (first match). Symlinked overlay
30
- # mirrors are resolved to their source file.
31
- # --from-report: batch mode over `policy-age-report.sh --json --candidates`
32
- # output; lists what would change and requires --yes to write.
33
- set -uo pipefail
34
- HQ_ROOT="${HQ_ROOT:-${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}}"
35
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
36
- VALIDATOR_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
37
- POLICY_VALIDATOR="$VALIDATOR_ROOT/.claude/hooks/validate-policy-frontmatter.sh"
38
- TARGET=""; REASON=""; BY="${HQ_SESSION_ID:-${USER:-unknown}}"; RESTORE=0; REPORT=""; CLASS=""; YES=0
39
- AUTO=0; DRY=0; AUTO_DIRS=(); REPORT_DIR=""; CLASSES=""
40
- while [ $# -gt 0 ]; do
41
- case "$1" in
42
- --auto) AUTO=1; shift ;;
43
- --dry-run) DRY=1; shift ;;
44
- --dir) AUTO_DIRS+=("$2"); shift 2 ;;
45
- --report-dir) REPORT_DIR="$2"; shift 2 ;;
46
- --classes) CLASSES="$2"; shift 2 ;; # comma list; default: all classes
47
- --reason) REASON="$2"; shift 2 ;;
48
- --by) BY="$2"; shift 2 ;;
49
- --restore) RESTORE=1; shift ;;
50
- --from-report) REPORT="$2"; shift 2 ;;
51
- --class) CLASS="$2"; shift 2 ;;
52
- --yes) YES=1; shift ;;
53
- -h|--help) sed -n 2,22p "$0"; exit 0 ;;
54
- -*) echo "policy-retire: unknown arg $1" >&2; exit 2 ;;
55
- *) TARGET="$1"; shift ;;
56
- esac
57
- done
58
- resolve() { # slug-or-path -> real file path or empty
59
- local t="$1" f
60
- if [ -f "$t" ]; then f="$t"
61
- else
62
- for d in "$HQ_ROOT/personal/policies" "$HQ_ROOT/core/policies" "$HQ_ROOT"/companies/*/policies; do
63
- [ -f "$d/$t.md" ] && { f="$d/$t.md"; break; }
64
- [ -f "$d/$t" ] && { f="$d/$t"; break; }
65
- done
66
- fi
67
- [ -n "${f:-}" ] || return 1
68
- [ -L "$f" ] && f="$(cd "$(dirname "$f")" && realpath "$(readlink "$f")" 2>/dev/null || readlink "$f")"
69
- printf '%s' "$f"
70
- }
71
- validate_policy_before_write() { # <policy-file>
72
- # Do not replicate frontmatter extraction here. The authoring hook validates
73
- # every resulting `when:` key (including duplicates), hard-policy limits,
74
- # and the canonical trigger grammar. A retirement write must meet that exact
75
- # contract or a sanctioned route could modify a policy normal authoring
76
- # rejects.
77
- local f="$1" payload result
78
- [ -f "$POLICY_VALIDATOR" ] || { echo "policy-retire: missing policy validator: $POLICY_VALIDATOR" >&2; return 1; }
79
- command -v jq >/dev/null 2>&1 || { echo "policy-retire: jq is required for canonical policy validation" >&2; return 1; }
80
- payload="$(jq -nc --arg file "$f" --rawfile content "$f" '{tool_input:{file_path:$file,content:$content}}')" \
81
- || { echo "policy-retire: could not prepare policy validation input for $f" >&2; return 1; }
82
- result="$(HQ_ROOT="$VALIDATOR_ROOT" CLAUDE_PROJECT_DIR="$VALIDATOR_ROOT" HQ_ALLOW_POLICY_NO_TRIGGER= \
83
- bash "$POLICY_VALIDATOR" <<<"$payload" 2>&1)" || {
84
- printf 'policy-retire: refusing to write %s; canonical policy validation failed:\n%s\n' "$f" "$result" >&2
85
- return 1
86
- }
87
- }
88
- set_status() { # <file> <retired|active> <reason>
89
- local f="$1" st="$2" why="$3" now; now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
90
- head -1 "$f" | grep -q '^---' || { echo "policy-retire: $f has no frontmatter" >&2; return 1; }
91
- validate_policy_before_write "$f" || return 1
92
- awk -v st="$st" -v why="$why" -v by="$BY" -v now="$now" '
93
- BEGIN { d=0; done=0 }
94
- /^---[ \t]*$/ { d++; if (d==2 && !done) {
95
- if (st=="retired") { print "status: retired"; print "retired_at: " now; print "retired_by: " by; print "retired_reason: \"" why "\"" }
96
- done=1 }
97
- print; next }
98
- d==1 && /^(status|retired_at|retired_by|retired_reason):/ { next }
99
- { print }' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
100
- }
101
- if [ "$AUTO" = 1 ]; then
102
- AGE="$HQ_ROOT/core/scripts/policy-age-report.sh"; [ -x "$AGE" ] || AGE="$(dirname "${BASH_SOURCE[0]}")/policy-age-report.sh"
103
- if [ "${#AUTO_DIRS[@]}" -eq 0 ]; then
104
- AUTO_DIRS=("$HQ_ROOT/personal/policies")
105
- co="$(bash "$HQ_ROOT/core/scripts/hq-session.sh" get company_slug 2>/dev/null || true)"
106
- [ -n "$co" ] && [ "$co" != "personal" ] && [ -d "$HQ_ROOT/companies/$co/policies" ] && AUTO_DIRS+=("$HQ_ROOT/companies/$co/policies")
107
- fi
108
- dirargs=(); for d in "${AUTO_DIRS[@]}"; do dirargs+=(--dir "$d"); done
109
- json="$(HQ_ROOT="$HQ_ROOT" bash "$AGE" "${dirargs[@]}" --json --candidates 2>/dev/null)" || json="[]"
110
- n="$(jq 'length' <<<"$json")"
111
- today="$(date -u +%Y-%m-%d)"; [ -n "$REPORT_DIR" ] || REPORT_DIR="$HQ_ROOT/workspace/reports"
112
- out="$REPORT_DIR/policy-retirement-$today.md"; mkdir -p "$REPORT_DIR"
113
- # One file per day; each run appends its own section so nothing is overwritten.
114
- : > "$out.tmp"
115
- if [ ! -f "$out" ]; then
116
- {
117
- echo "# Automatic policy retirement — $today"; echo
118
- echo "Mode: human on the loop. Each policy below was retired by \`policy-retire.sh --auto\` with the reason shown."
119
- echo "Reverse any wrong call with \`bash core/scripts/policy-retire.sh <slug> --restore\` and fix the rule that produced it."
120
- echo
121
- } > "$out.tmp"
122
- fi
123
- { echo "## Run $(date -u +%H:%M:%SZ) — dirs: ${AUTO_DIRS[*]}"; echo; } >> "$out.tmp"
124
- retired=0; failed=0; needfix=0; would_retire=0
125
- while IFS=$' ' read -r id path classes fired last age stale judged stale_n renamed; do
126
- [ -n "$path" ] || continue
127
- f="$HQ_ROOT/$path"; [ -f "$f" ] || f="$path"; [ -f "$f" ] || continue
128
- grep -q '^status: retired' "$f" && continue
129
- if [ -n "$CLASSES" ]; then
130
- keep=""; for c in $(tr ',' ' ' <<<"$classes"); do case ",$CLASSES," in *,$c,*) keep="${keep:+$keep,}$c" ;; esac; done
131
- classes="$keep"; [ -n "$classes" ] || continue
132
- fi
133
- why=""
134
- # stale-refs retires only when EVERY judged reference is gone — the rule's
135
- # whole subject vanished. A partly stale rule is reported for a fix, not retired.
136
- case ",$classes," in *,renamed-refs,*) printf -- '- needs-fix (not retired) **%s** (%s): referenced file was renamed: %s\n' "$id" "$path" "$renamed" >> "$out.tmp"; needfix=$((needfix+1)) ;; esac
137
- case ",$classes," in *,stale-refs,*)
138
- if [ "${judged:-0}" -gt 0 ] && [ "${stale_n:-0}" -eq "${judged:-0}" ]; then why="every path the rule depends on is gone ($stale)"
139
- else printf -- '- needs-fix (not retired) **%s** (%s): references gone: %s\n' "$id" "$path" "$stale" >> "$out.tmp"; needfix=$((needfix+1)); fi ;;
140
- esac
141
- case ",$classes," in *,never-fired,*) why="${why:+$why; }never emitted in any session ledger over ${age} days" ;; esac
142
- case ",$classes," in *,dormant,*) why="${why:+$why; }last emitted $last, more than the dormant window ago" ;; esac
143
- case ",$classes," in *,single-incident-old,*) why="${why:+$why; }one-off correction, ${age} days old, fired ${fired}x" ;; esac
144
- [ -n "$why" ] || continue
145
- if [ "$DRY" = 1 ]; then
146
- echo "would retire: $id — $why"
147
- would_retire=$((would_retire+1))
148
- continue
149
- fi
150
- if set_status "$f" retired "auto: $why"; then
151
- echo "retired: $id — $why"
152
- printf -- '- **%s** (%s): %s\n' "$id" "$path" "$why" >> "$out.tmp"
153
- retired=$((retired+1))
154
- else
155
- echo "failed: $id ($path); policy was not retired" >&2
156
- printf -- '- failed (not retired) **%s** (%s): canonical policy validation failed\n' "$id" "$path" >> "$out.tmp"
157
- failed=$((failed+1))
158
- fi
159
- done < <(jq -r '.[] | [.id, .path, (.candidate_classes|join(",")), (.fired|tostring), (.last_fired // "never"), (.age_days|tostring), (.stale_refs|join(", ")), ((.refs_judged // 0)|tostring), ((.stale_refs|length)|tostring), ((.renamed_refs // [])|join(", "))] | @tsv' <<<"$json")
160
- if [ "$DRY" = 1 ]; then rm -f "$out.tmp"; echo "policy-retire --auto: $would_retire of $n candidates would be retired (dry run)"
161
- else
162
- if [ "$retired" -gt 0 ] || [ "$failed" -gt 0 ] || [ "$needfix" -gt 0 ]; then cat "$out.tmp" >> "$out"; rm -f "$out.tmp"; echo "policy-retire --auto: $retired retired, $failed failed, $needfix need a fix; report ${out#"$HQ_ROOT"/}"; else rm -f "$out.tmp"; echo "policy-retire --auto: nothing to retire"; fi
163
- fi
164
- [ "$failed" -eq 0 ] || exit 1
165
- exit 0
166
- fi
167
- if [ -n "$REPORT" ]; then
168
- [ -n "$CLASS" ] && [ -n "$REASON" ] || { echo "--from-report needs --class and --reason" >&2; exit 2; }
169
- [ -r "$REPORT" ] || { echo "cannot read $REPORT" >&2; exit 2; }
170
- retired=0; failed=0; selected=0
171
- while IFS=$'\t' read -r id path; do
172
- [ -n "$path" ] || continue
173
- f="$HQ_ROOT/$path"; [ -f "$f" ] || continue
174
- grep -q '^status: retired' "$f" && continue
175
- selected=$((selected+1))
176
- if [ "$YES" = 1 ]; then
177
- if set_status "$f" retired "$REASON [$CLASS]"; then
178
- retired=$((retired+1)); echo "retired: $id ($path)"
179
- else
180
- failed=$((failed+1)); echo "failed: $id ($path); policy was not retired" >&2
181
- fi
182
- else echo "would retire: $id ($path)"; fi
183
- done < <(jq -r --arg c "$CLASS" '.[] | select(.candidate_classes | index($c)) | "\(.id)\t\(.path)"' "$REPORT")
184
- if [ "$YES" = 1 ]; then
185
- echo "policy-retire: $retired retired [$CLASS]; $failed failed"
186
- [ "$failed" -eq 0 ] || exit 1
187
- else
188
- echo "policy-retire: $selected would be retired [$CLASS] (add --yes)"
189
- fi
190
- exit 0
191
- fi
192
- [ -n "$TARGET" ] || { sed -n 2,22p "$0"; exit 2; }
193
- F="$(resolve "$TARGET")" || { echo "policy-retire: no policy found for '$TARGET'" >&2; exit 3; }
194
- if [ "$RESTORE" = 1 ]; then
195
- set_status "$F" active "" && echo "policy-retire: restored ${F#"$HQ_ROOT"/}"
196
- else
197
- [ -n "$REASON" ] || { echo "policy-retire: --reason is required" >&2; exit 2; }
198
- set_status "$F" retired "$REASON" && echo "policy-retire: retired ${F#"$HQ_ROOT"/} — $REASON"
199
- fi
@@ -1,39 +0,0 @@
1
- #!/bin/bash
2
- # read-policy-frontmatter.sh — Extract YAML frontmatter from a policy file
3
- #
4
- # Usage: bash core/scripts/read-policy-frontmatter.sh <policy-file>
5
- # bash core/scripts/read-policy-frontmatter.sh <policy-file> <policy-file> ...
6
- #
7
- # Returns only the YAML frontmatter block (between the first two --- markers),
8
- # skipping the policy body. Used by /startwork, /plan, /brainstorm, /run commands
9
- # to minimize context burn when scanning policy metadata (id, title, enforcement,
10
- # when) without loading the full ## Rule and ## Rationale sections.
11
- # (Fields id/title/enforcement/when/on live in that block.)
12
- #
13
- # Exit codes:
14
- # 0 - Success, frontmatter written to stdout
15
- # 1 - Usage error or file not readable
16
-
17
- set -euo pipefail
18
-
19
- if [ $# -lt 1 ]; then
20
- echo "USAGE: read-policy-frontmatter.sh <policy-file>" >&2
21
- exit 1
22
- fi
23
-
24
- INDEX=0
25
- for FILE in "$@"; do
26
- if [ ! -r "$FILE" ]; then
27
- echo "ERROR: File not readable: $FILE" >&2
28
- exit 1
29
- fi
30
-
31
- if [ "$INDEX" -gt 0 ]; then
32
- printf '\n# --- policy-file: %s ---\n' "$FILE"
33
- fi
34
- INDEX=$((INDEX + 1))
35
-
36
- # Extract frontmatter: lines between first two --- markers.
37
- # c counts --- markers; print lines only while inside the frontmatter (c==1).
38
- awk '/^---$/{c++; next} c==1' "$FILE"
39
- done