@indigoai-us/hq-cli 5.312.1 → 5.312.2-beta.0

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +2 -5
  2. package/dist/command-catalog.generated.d.ts +0 -65
  3. package/dist/command-catalog.generated.js +0 -80
  4. package/dist/commands/cloud.js +1 -0
  5. package/dist/commands/company.js +0 -2
  6. package/dist/commands/core.js +0 -7
  7. package/dist/fast-core.js +3 -10
  8. package/dist/fast-scaffold.d.ts +0 -2
  9. package/dist/fast-scaffold.js +0 -11
  10. package/dist/lib/doctor/checks/work-context.js +1 -9
  11. package/dist/lib/mesh/client.js +5 -23
  12. package/dist/lib/mesh/live/daemon/doctor.d.ts +0 -2
  13. package/dist/lib/mesh/live/daemon/doctor.js +1 -11
  14. package/dist/lib/search-index/embed-lock.d.ts +0 -2
  15. package/dist/lib/search-index/embed-lock.js +1 -10
  16. package/dist/lib/work-context/contract.d.ts +0 -3
  17. package/dist/lib/work-context/contract.schema.json +1 -3
  18. package/dist/lib/work-context/outbox.d.ts +5 -14
  19. package/dist/lib/work-context/outbox.js +5 -48
  20. package/dist/lib/work-context/reconcile.js +5 -26
  21. package/dist/main.d.ts +0 -20
  22. package/dist/main.js +1 -8
  23. package/package.json +1 -1
  24. package/assets/scaffold/core/scripts/lib/work-mesh-live-hook.sh +0 -378
  25. package/assets/scaffold/core/scripts/lib/work-mesh-live-rebind.sh +0 -141
  26. package/assets/scaffold/core/scripts/migrate-policy-triggers.sh +0 -373
  27. package/assets/scaffold/core/scripts/register-project.sh +0 -262
  28. package/assets/scaffold/core/scripts/work-mesh-live-rebind.sh +0 -59
  29. package/dist/commands/company-rename-flag.d.ts +0 -4
  30. package/dist/commands/company-rename-flag.js +0 -7
  31. package/dist/commands/company-rename.d.ts +0 -4
  32. package/dist/commands/company-rename.js +0 -90
  33. package/dist/commands/core-eval-trigger-native.d.ts +0 -4
  34. package/dist/commands/core-eval-trigger-native.js +0 -12
  35. package/dist/commands/core-migrate-policy-triggers-native.d.ts +0 -21
  36. package/dist/commands/core-migrate-policy-triggers-native.js +0 -378
  37. package/dist/commands/core-native-hook-helper-commands.d.ts +0 -4
  38. package/dist/commands/core-native-hook-helper-commands.js +0 -56
  39. package/dist/commands/core-native-hook-helpers-flag.d.ts +0 -4
  40. package/dist/commands/core-native-hook-helpers-flag.js +0 -7
  41. package/dist/commands/core-register-project-native.d.ts +0 -30
  42. package/dist/commands/core-register-project-native.js +0 -347
  43. package/dist/commands/core-work-mesh-live-rebind-native.d.ts +0 -18
  44. package/dist/commands/core-work-mesh-live-rebind-native.js +0 -262
  45. package/dist/lib/mesh/denial-codes.d.ts +0 -5
  46. package/dist/lib/mesh/denial-codes.js +0 -14
  47. package/dist/utils/core-eval-trigger.d.ts +0 -4
  48. package/dist/utils/core-eval-trigger.js +0 -88
@@ -1,141 +0,0 @@
1
- #!/usr/bin/env bash
2
- # hq-core: public
3
- # US-011 — mid-session rebind: session_end (old) + session_start (new) in spool.
4
- # Sourced by hooks/scripts. Never execute directly.
5
-
6
- # work_mesh_live_binding_marker_path <sid>
7
- work_mesh_live_binding_marker_path() {
8
- printf '%s/.hq/work-context/sessions/%s.live-binding' "${WORK_MESH_HOME:-$HOME}" "$1"
9
- }
10
-
11
- # work_mesh_live_read_binding_marker <sid>
12
- # Sets REPLY to "company|project|task" (may be empty fields).
13
- work_mesh_live_read_binding_marker() {
14
- local sid="$1" path body
15
- path="$(work_mesh_live_binding_marker_path "$sid")"
16
- REPLY="||"
17
- [ -f "$path" ] || return 0
18
- body="$(tr -d '\r' <"$path" 2>/dev/null || true)"
19
- # companySlug=.. project=.. task=..
20
- local co="" pr="" tk=""
21
- co="$(printf '%s\n' "$body" | awk -F= '/^companySlug=/{print $2; exit}')"
22
- pr="$(printf '%s\n' "$body" | awk -F= '/^project=/{print $2; exit}')"
23
- tk="$(printf '%s\n' "$body" | awk -F= '/^task=/{print $2; exit}')"
24
- REPLY="${co}|${pr}|${tk}"
25
- }
26
-
27
- # work_mesh_live_write_binding_marker <sid> <company> <project> <task>
28
- work_mesh_live_write_binding_marker() {
29
- local sid="$1" co="${2:-}" pr="${3:-}" tk="${4:-}" path dir
30
- path="$(work_mesh_live_binding_marker_path "$sid")"
31
- dir="${path%/*}"
32
- mkdir -p -- "$dir" 2>/dev/null || true
33
- chmod 700 -- "$dir" 2>/dev/null || true
34
- printf 'companySlug=%s\nproject=%s\ntask=%s\n' "$co" "$pr" "$tk" >"$path"
35
- chmod 600 -- "$path" 2>/dev/null || true
36
- }
37
-
38
- # work_mesh_live_enqueue_lifecycle <kind> <sid> <harness> <adapter> <company> <project> <task>
39
- work_mesh_live_enqueue_lifecycle() {
40
- local kind="$1" sid="$2" harness="$3" adapter="$4"
41
- local company="${5:-}" project="${6:-}" task="${7:-}"
42
- local seq
43
- if ! command -v work_mesh_enqueue >/dev/null 2>&1; then
44
- return 1
45
- fi
46
- if command -v work_mesh_live_next_seq >/dev/null 2>&1; then
47
- seq="$(work_mesh_live_next_seq "$sid")" || return 1
48
- else
49
- seq=1
50
- fi
51
- local args=(
52
- --kind "$kind"
53
- --session-id "$sid"
54
- --harness "$harness"
55
- --adapter-version "$adapter"
56
- --seq "$seq"
57
- --cwd "${PWD:-}"
58
- --hq-root "${HQ_ROOT:-}"
59
- )
60
- [ -n "$company" ] && args+=(--company-slug "$company")
61
- [ -n "$project" ] && args+=(--project "$project")
62
- [ -n "$task" ] && args+=(--task "$task")
63
- work_mesh_enqueue "${args[@]}" || true
64
- }
65
-
66
- # work_mesh_live_rebind <sid> <old_co> <old_pr> <old_tk> <new_co> <new_pr> <new_tk>
67
- # Always appends session_end then session_start when old project differs from new
68
- # and new project is non-empty. Updates the live-binding marker to the new binding.
69
- work_mesh_live_rebind() {
70
- local sid="$1"
71
- local old_co="${2:-}" old_pr="${3:-}" old_tk="${4:-}"
72
- local new_co="${5:-}" new_pr="${6:-}" new_tk="${7:-}"
73
- local harness adapter
74
- [ -n "$sid" ] || return 1
75
- [ -n "$new_pr" ] || return 1
76
- [ "$old_pr" != "$new_pr" ] || {
77
- work_mesh_live_write_binding_marker "$sid" "$new_co" "$new_pr" "$new_tk"
78
- return 0
79
- }
80
-
81
- harness="${HQ_HARNESS:-${HQ_WORK_MESH_HARNESS:-${HQ_CHECKPOINT_RUNTIME:-claude-code}}}"
82
- case "$harness" in
83
- claude|Claude|ClaudeCode) harness=claude-code ;;
84
- Codex) harness=codex ;;
85
- Grok) harness=grok ;;
86
- esac
87
- adapter="${HQ_ADAPTER_CONTRACT_VERSION:-1.0.0}"
88
-
89
- # Event order: session_end then session_start. Each call takes the next seq from
90
- # the same per-session counter, so session_start always has a strictly higher
91
- # seq than session_end. The server REOPENs a session when a session_start with
92
- # a higher seq and a new binding follows the session_end.
93
- if [ -n "$old_pr" ]; then
94
- work_mesh_live_enqueue_lifecycle session_end "$sid" "$harness" "$adapter" \
95
- "$old_co" "$old_pr" "$old_tk"
96
- fi
97
- work_mesh_live_enqueue_lifecycle session_start "$sid" "$harness" "$adapter" \
98
- "$new_co" "$new_pr" "$new_tk"
99
- work_mesh_live_write_binding_marker "$sid" "$new_co" "$new_pr" "$new_tk"
100
- return 0
101
- }
102
-
103
- # work_mesh_live_maybe_rebind_from_state <sid>
104
- # Compare state file projectId to live-binding marker; rebind if changed.
105
- work_mesh_live_maybe_rebind_from_state() {
106
- local sid="$1" state co pr tk old old_co old_pr old_tk
107
- local wc="${WORK_MESH_HOME:-$HOME}"
108
- state="$wc/.hq/work-context/sessions/$sid.json"
109
- [ -f "$state" ] || return 0
110
-
111
- if command -v jq >/dev/null 2>&1; then
112
- co="$(jq -r '.companySlug // empty' "$state" 2>/dev/null || true)"
113
- pr="$(jq -r '.projectId // empty' "$state" 2>/dev/null || true)"
114
- tk="$(jq -r '.taskId // empty' "$state" 2>/dev/null || true)"
115
- else
116
- return 0
117
- fi
118
- [ -n "$pr" ] || return 0
119
-
120
- work_mesh_live_read_binding_marker "$sid"
121
- old=$REPLY
122
- old_co="${old%%|*}"
123
- rest="${old#*|}"
124
- old_pr="${rest%%|*}"
125
- old_tk="${rest#*|}"
126
-
127
- if [ -z "$old_pr" ]; then
128
- # First bind observed — record marker without closing a card (session_start
129
- # already fired at SessionStart). Still emit session_start when marker empty
130
- # but only if organize rebound after an unbound start: treat empty old as
131
- # "no prior card" and only update marker unless caller forces rebind.
132
- work_mesh_live_write_binding_marker "$sid" "$co" "$pr" "$tk"
133
- return 0
134
- fi
135
- if [ "$old_pr" = "$pr" ]; then
136
- # Same project — refresh task/company on marker.
137
- work_mesh_live_write_binding_marker "$sid" "$co" "$pr" "$tk"
138
- return 0
139
- fi
140
- work_mesh_live_rebind "$sid" "$old_co" "$old_pr" "$old_tk" "$co" "$pr" "$tk"
141
- }
@@ -1,373 +0,0 @@
1
- #!/bin/bash
2
- # migrate-policy-triggers.sh — add `when:`/`on:` frontmatter to every policy
3
- # that lacks it, derived from the policy's own metadata.
4
- #
5
- # NOTE: `trigger:` was removed from the policy schema (superseded by when/on).
6
- # This backfill still reads a `trigger:` line if a legacy/external policy has
7
- # one, but newly authored policies derive `when:` from tags alone. When no
8
- # signal can be derived, only hard-enforcement policies receive the
9
- # `when: always` fallback; soft/unset policies remain untriggered. The
10
- # create/edit guard validate-policy-frontmatter.sh is now the primary
11
- # enforcement that every authored policy declares when/on.
12
- #
13
- # `when:` is generated from up to TWO sources, OR-combined:
14
- # 1. trigger content — the authored `trigger:` prose, mapped to a precise
15
- # expression for the cross-cutting actions tags don't capture (git
16
- # sub-commands, deploy, secret/credential, migration, pr, install, ...).
17
- # Compound expressions (`git && push`) are preserved verbatim.
18
- # 2. tags — the `tags: [...]` frontmatter, the policy's topical vocabulary
19
- # (git, deploy, slack, supabase, auth, refactor, ...). `vendor:x` is
20
- # normalised to `x`; pure taxonomy/meta tags that never appear in a prompt
21
- # or command (infrastructure, consolidated, ux, safety, ...) are dropped so
22
- # they don't bloat the expression or over-fire on a generic word.
23
- #
24
- # Because fact derivation is now OPEN (derive-trigger-facts.sh emits every word
25
- # token, not a fixed list), any surviving tag/trigger word is a live token — no
26
- # expression is dead.
27
- #
28
- # `on:` = ALL hook events EXCEPT SessionStart — [PreToolUse, PostToolUse,
29
- # UserPromptSubmit, AssistantIntent] — for any policy that gets a real `when:`.
30
- # `on:` is the set of sites where the policy is even evaluated; the `when:`
31
- # expression does the actual filtering, so evaluating broadly is cheap and
32
- # correct. SessionStart is reserved for the hard-policy `when: always` fallback
33
- # (it has no command/prompt facts — only static facts + `always`), used when
34
- # neither tags nor trigger yield a signal. A non-hard policy with no derivable
35
- # signal is left unchanged so it cannot silently join the SessionStart baseline.
36
- #
37
- # STRICTLY IDEMPOTENT: a policy that already has a `when:` line is left
38
- # untouched — the script only ever ADDS when/on to a policy that lacks them, and
39
- # never rewrites an existing trigger. There is no force/regenerate mode: once a
40
- # policy declares (or a human tunes) its trigger, that is authoritative.
41
- #
42
- # Runs at SessionStart (registered in .claude/settings.json) so any newly
43
- # authored policy — personal, company, or repo — is auto-backfilled with a
44
- # trigger on the next session, with zero writes in steady state.
45
- #
46
- # Edits in place via this script (Bash) — NOT the Edit tool — to avoid per-file
47
- # autosave churn.
48
- #
49
- # Usage: bash core/scripts/migrate-policy-triggers.sh [--dry-run] [dir ...]
50
- # With no dir, scans core/policies plus the active company/repo policy dir
51
- # derived from the cwd (the SessionStart behaviour). Pass dir(s) to override.
52
- #
53
- # COOLDOWN
54
- #
55
- # A full policy scan is idempotent but expensive, and this script runs on every
56
- # SessionStart. Completed non-dry runs are therefore limited to one per
57
- # cooldown window (default 60 minutes). The stamp records only a SUCCESSFUL
58
- # completion — never a run in progress — so an interrupted SessionStart cannot
59
- # suppress the next migration.
60
- #
61
- # Env overrides:
62
- # HQ_MIGRATE_POLICY_TRIGGERS_COOLDOWN_SECONDS default 3600; 0 disables cooldown
63
- # HQ_MIGRATE_POLICY_TRIGGERS_FORCE=1 run now regardless of cooldown
64
- # HQ_MIGRATE_POLICY_TRIGGERS_STATE_DIR where the per-host stamp lives
65
- #
66
- # The stamp is per-host runtime state, so it lives under $XDG_STATE_HOME
67
- # (default $HOME/.local/state), never under this repo or workspace/, which HQ
68
- # Sync reconciles. It is scoped to both HQ_ROOT and the resolved policy
69
- # directories, so one tenant cannot suppress another tenant's first scan. If
70
- # neither a state override, XDG_STATE_HOME, nor HOME is available, the script
71
- # safely runs without a cooldown. --dry-run neither consults nor updates it.
72
-
73
- set -uo pipefail
74
-
75
- HQ_ROOT="${HQ_ROOT:-${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}}"
76
- CWD="${CLAUDE_PROJECT_DIR:-$PWD}"
77
- DRY=0; DIRS=()
78
- for a in "$@"; do case "$a" in
79
- --dry-run) DRY=1 ;;
80
- *) DIRS+=("$a") ;;
81
- esac; done
82
-
83
- # Default scope: global core and personal policies, every personal worker
84
- # profile, and only the active company's and repo's policies. Company worker
85
- # profiles remain tenant-scoped to the active company.
86
- if [ "${#DIRS[@]}" -eq 0 ]; then
87
- # personal/policies is read DIRECTLY (the reindex symlink mirror into
88
- # core/policies is retired). Mirrors the scope inject-policy-on-trigger reads.
89
- DIRS=("$HQ_ROOT/core/policies" "$HQ_ROOT/personal/policies")
90
- for d in "$HQ_ROOT"/personal/workers/*/policies; do
91
- [ -d "$d" ] && DIRS+=("$d")
92
- done
93
- co="${HQ_POLICY_COMPANY:-}"
94
- if [ -z "$co" ]; then
95
- case "$CWD" in
96
- *companies/*) co="$(printf '%s' "$CWD" | sed -nE 's#.*companies/([^/]+).*#\1#p')" ;;
97
- esac
98
- fi
99
- case "$co" in
100
- ''|.|..|*/*) ;;
101
- *)
102
- DIRS+=("$HQ_ROOT/companies/$co/policies")
103
- for d in "$HQ_ROOT/companies/$co"/workers/*/policies; do
104
- [ -d "$d" ] && DIRS+=("$d")
105
- done
106
- ;;
107
- esac
108
- case "$CWD" in
109
- *repos/public/*|*repos/private/*)
110
- rscope="$(printf '%s' "$CWD" | sed -nE 's#.*repos/(public|private)/.*#\1#p')"
111
- rname="$(printf '%s' "$CWD" | sed -nE 's#.*repos/[^/]+/([^/]+).*#\1#p')"
112
- [ -n "$rscope" ] && [ -n "$rname" ] && DIRS+=("$HQ_ROOT/repos/$rscope/$rname/.claude/policies") ;;
113
- esac
114
- fi
115
-
116
- STATE_DIR="${HQ_MIGRATE_POLICY_TRIGGERS_STATE_DIR:-}"
117
- if [ -z "$STATE_DIR" ]; then
118
- if [ -n "${XDG_STATE_HOME:-}" ]; then
119
- STATE_DIR="$XDG_STATE_HOME/hq-migrate-policy-triggers"
120
- elif [ -n "${HOME:-}" ]; then
121
- STATE_DIR="$HOME/.local/state/hq-migrate-policy-triggers"
122
- fi
123
- fi
124
- COOLDOWN_SECONDS="${HQ_MIGRATE_POLICY_TRIGGERS_COOLDOWN_SECONDS:-3600}"
125
- STAMP=""
126
- LOCK=""
127
- STATE_READY=0
128
-
129
- # Hash the resolved scope rather than using a shared per-root stamp: the
130
- # default scan deliberately includes tenant-specific directories.
131
- scope_key() {
132
- if command -v sha256sum >/dev/null 2>&1; then
133
- printf '%s\0' "$HQ_ROOT" "${DIRS[@]}" | sha256sum | awk '{print $1}'
134
- elif command -v shasum >/dev/null 2>&1; then
135
- printf '%s\0' "$HQ_ROOT" "${DIRS[@]}" | shasum -a 256 | awk '{print $1}'
136
- else
137
- return 1
138
- fi
139
- }
140
-
141
- if [ -n "$STATE_DIR" ] && SCOPE_KEY="$(scope_key)"; then
142
- STAMP="$STATE_DIR/$SCOPE_KEY/last-success"
143
- LOCK="$STATE_DIR/$SCOPE_KEY/migrate.lock"
144
- mkdir -p "$(dirname "$STAMP")" 2>/dev/null && STATE_READY=1
145
- fi
146
-
147
- cooldown_note() {
148
- printf 'migrate-policy-triggers: cooldown stamp %s; running\n' "$1" >&2
149
- }
150
-
151
- # Three stamp states are intentionally distinct:
152
- # absent -> this scope has not completed a run; run
153
- # present but unreadable/future -> cannot trust the clock; log and run
154
- # present and fresh -> exit quietly
155
- cooldown_blocks_run() {
156
- [ "$DRY" = "1" ] && return 1
157
- [ "$STATE_READY" = "1" ] || return 1
158
- [ "${HQ_MIGRATE_POLICY_TRIGGERS_FORCE:-0}" = "1" ] && return 1
159
- [ "$COOLDOWN_SECONDS" -gt 0 ] 2>/dev/null || return 1
160
- { [ -e "$STAMP" ] || [ -L "$STAMP" ]; } || return 1
161
-
162
- local last now age
163
- last="$(cat "$STAMP" 2>/dev/null)"
164
- case "$last" in
165
- ''|*[!0-9]*) cooldown_note "is unreadable"; return 1 ;;
166
- esac
167
-
168
- now="$(date -u '+%s')"
169
- age=$((now - last))
170
- if [ "$age" -lt 0 ]; then
171
- cooldown_note "is in the future"
172
- return 1
173
- fi
174
- [ "$age" -lt "$COOLDOWN_SECONDS" ]
175
- }
176
-
177
- write_success_stamp() {
178
- local stamp_tmp
179
- [ "$DRY" = "1" ] && return 0
180
- [ "$STATE_READY" = "1" ] || return 0
181
- stamp_tmp="$(mktemp "$(dirname "$STAMP")/.last-success.XXXXXX")" || return 0
182
- if printf '%s\n' "$(date -u '+%s')" >"$stamp_tmp" && mv "$stamp_tmp" "$STAMP"; then
183
- return 0
184
- fi
185
- rm -f "$stamp_tmp"
186
- }
187
-
188
- if cooldown_blocks_run; then
189
- exit 0
190
- fi
191
-
192
- # A scope-local advisory lock prevents concurrent SessionStart hooks from both
193
- # observing the same absent/stale stamp. Recheck after acquiring it because a
194
- # prior holder may have completed while this invocation waited to acquire it.
195
- if [ "$STATE_READY" = "1" ] && [ "$DRY" != "1" ] && command -v flock >/dev/null 2>&1; then
196
- if ! exec 9>"$LOCK"; then
197
- exit 0
198
- fi
199
- if ! flock -n 9; then # command -v flock checked above
200
- exit 0
201
- fi
202
- if cooldown_blocks_run; then
203
- exit 0
204
- fi
205
- fi
206
-
207
- ON_LIVE="[PreToolUse, PostToolUse, UserPromptSubmit, AssistantIntent]"
208
- ON_START="[SessionStart]"
209
-
210
- # A synthesized `when:` is written to disk without a human ever reading it, and
211
- # the write-time validator does not see a `bash`-authored edit. So every derived
212
- # expression is checked against the canonical grammar before it lands — a tag
213
- # token carrying a character outside the identifier charset would otherwise
214
- # produce a `when:` the evaluator cannot parse, and the policy would never match
215
- # for the reason it was backfilled.
216
- EVAL_TRIGGER="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/eval-trigger.sh"
217
- when_parses() {
218
- [ -f "$EVAL_TRIGGER" ] || return 0 # no evaluator -> don't block the backfill
219
- [ "$(printf 'x\t%s\n' "$1" | bash "$EVAL_TRIGGER" --check | cut -f2)" = "ok" ]
220
- }
221
-
222
- # Meta / taxonomy tags that classify a policy but never appear as a word in a
223
- # command, prompt, or AI message — dropping them keeps `when:` to live signals.
224
- TAG_STOP=" infrastructure consolidated ux safety hq hq-core hq-cli hq-packages \
225
- hq-discipline basic-users knowledge-repos quiet-mode capabilities narration \
226
- voice intent delight orchestration abstraction workflow deliverable routing \
227
- anchor rulesets data-handling shell-injection promotion docs knowledge "
228
-
229
- # field extractor (first frontmatter occurrence)
230
- fm() { awk -v k="$1" '/^---$/{d++;next} d==1 && $0 ~ ("^" k ":"){sub("^" k ":[[:space:]]*","");print;exit}' "$2"; }
231
-
232
- # trigger_expr <lowercased-trigger-prose> — precise expression for a known
233
- # action, or empty string. Mirrors the tokens derive-trigger-facts.sh emits.
234
- trigger_expr() {
235
- local t="$1"
236
- case "$t" in
237
- *deploy*) echo 'deploy' ;;
238
- *credential*|*secret*|*op://*|*aws_profile*|*.env*) echo 'secret || credential' ;;
239
- *"git push"*|*"gh pr merge"*|*" push "*|*"push to"*) echo 'git && push' ;;
240
- *commitment*) echo '' ;; # "commitment(s)" is NOT a git commit — let tags drive
241
- *commit*) echo 'git && commit' ;;
242
- *checkout*) echo 'git && checkout' ;;
243
- *rebase*) echo 'git && rebase' ;;
244
- *"git stash"*|*" stash "*) echo 'git && stash' ;;
245
- *"gh pr"*|*"pull request"*|*"pr merge"*) echo 'pr' ;;
246
- *merge*) echo 'git && merge' ;;
247
- *migration*|*migrate*|*schema*|*prisma*) echo 'migrate || migration || schema' ;;
248
- *"npm install"*|*"pnpm "*|*"yarn "*|*" install "*|*"package install"*) echo 'install' ;;
249
- *grep*) echo 'grep' ;;
250
- *slack*) echo 'slack' ;;
251
- *email*) echo 'email' ;;
252
- *" git "*|"git "*|*" git") echo 'git' ;;
253
- *) echo '' ;;
254
- esac
255
- }
256
-
257
- # tag_tokens <raw-tags-line-without-brackets> — normalised, filtered tag tokens
258
- # (one per line). vendor:x -> x; meta tags dropped; lowercased.
259
- tag_tokens() {
260
- printf '%s' "$1" | tr ',' '\n' | while IFS= read -r tag; do
261
- tag="$(printf '%s' "$tag" | tr '[:upper:]' '[:lower:]' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
262
- tag="${tag#vendor:}" # vendor:linear -> linear
263
- [ -n "$tag" ] || continue
264
- case "$TAG_STOP" in *" $tag "*) continue ;; esac
265
- printf '%s\n' "$tag"
266
- done
267
- }
268
-
269
- # build_when <trigger-prose> <raw-tags> <enforcement> — emit "WHEN<TAB>ON".
270
- # Returns 1 without output when no signal is derivable for a non-hard policy.
271
- build_when() {
272
- local trig tags enforcement te t seen
273
- trig="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')"
274
- tags="$2"
275
- enforcement="${3%%#*}"
276
- enforcement="$(printf '%s' "$enforcement" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
277
- enforcement="${enforcement#\"}"; enforcement="${enforcement%\"}"
278
- enforcement="${enforcement#\'}"; enforcement="${enforcement%\'}"
279
- te="$(trigger_expr "$trig")"
280
-
281
- # terms: the trigger expression first (parenthesised if it has operators),
282
- # then each tag token not already named inside the trigger expression.
283
- local -a terms=()
284
- seen=" "
285
- if [ -n "$te" ]; then
286
- # record identifiers already used so a tag doesn't loosen a precise expr
287
- for t in $(printf '%s' "$te" | grep -oE '[a-z_][a-z0-9_./-]*'); do seen="$seen$t "; done
288
- case "$te" in *" "*) terms+=("( $te )") ;; *) terms+=("$te") ;; esac
289
- fi
290
- while IFS= read -r t; do
291
- [ -n "$t" ] || continue
292
- case "$seen" in *" $t "*) continue ;; esac
293
- seen="$seen$t "
294
- terms+=("$t")
295
- done < <(tag_tokens "$tags")
296
-
297
- if [ "${#terms[@]}" -eq 0 ]; then
298
- [ "$enforcement" = "hard" ] || return 1
299
- printf 'always\t%s' "$ON_START"
300
- else
301
- local when=""; local i
302
- for i in "${!terms[@]}"; do
303
- [ -z "$when" ] && when="${terms[$i]}" || when="$when || ${terms[$i]}"
304
- done
305
- printf '%s\t%s' "$when" "$ON_LIVE"
306
- fi
307
- }
308
-
309
- policy_has_when() {
310
- local line
311
- while IFS= read -r line || [ -n "$line" ]; do
312
- case "$line" in
313
- when:*) return 0 ;;
314
- esac
315
- done < "$1"
316
- return 1
317
- }
318
-
319
- total=0; migrated=0; skipped=0; untriggered=0; unparseable=0
320
- declare -i n_session=0
321
- for dir in "${DIRS[@]}"; do
322
- [ -d "$dir" ] || continue
323
- for f in "$dir"/*.md; do
324
- [ -f "$f" ] || continue
325
- file_name="${f##*/}"
326
- case "$file_name" in example-policy.md|README.md) continue ;; esac
327
- total=$((total+1))
328
- # STRICTLY IDEMPOTENT: a policy that already declares a trigger is left as-is.
329
- if policy_has_when "$f"; then skipped=$((skipped+1)); continue; fi
330
-
331
- trig="$(fm trigger "$f")"
332
- tags="$(fm tags "$f" | sed 's/^\[//; s/\]$//')"
333
- enforcement="$(fm enforcement "$f")"
334
- if ! built="$(build_when "$trig" "$tags" "$enforcement")"; then
335
- untriggered=$((untriggered+1))
336
- continue
337
- fi
338
- IFS=$'\t' read -r WHEN ON <<< "$built"
339
- if ! when_parses "$WHEN"; then
340
- unparseable=$((unparseable+1))
341
- printf 'migrate-policy-triggers: refusing to write unparseable when: %s -> `%s`\n' \
342
- "$file_name" "$WHEN" >&2
343
- continue
344
- fi
345
- [ "$ON" = "$ON_START" ] && n_session=$((n_session+1))
346
-
347
- if [ "$DRY" = "1" ]; then
348
- printf '%-52s when: %-44s on: %s\n' "$file_name" "$WHEN" "$ON"
349
- migrated=$((migrated+1)); continue
350
- fi
351
-
352
- # insert when:/on: after trigger: (or id: if no trigger line exists)
353
- anchor='^trigger:'; grep -q '^trigger:' "$f" || anchor='^id:'
354
- tmp="$(mktemp)" || exit 1
355
- if ! awk -v w="$WHEN" -v o="$ON" -v anc="$anchor" '
356
- { print }
357
- !ins && $0 ~ anc { print "when: " w; print "on: " o; ins=1 }
358
- ' "$f" > "$tmp"; then
359
- rm -f "$tmp"
360
- exit 1
361
- fi
362
- if ! mv "$tmp" "$f"; then
363
- rm -f "$tmp"
364
- exit 1
365
- fi
366
- migrated=$((migrated+1))
367
- done
368
- done
369
-
370
- # Quiet in steady state: only report when something was actually backfilled.
371
- [ "$migrated" -gt 0 ] && echo "migrate-policy-triggers: backfilled $migrated policy trigger(s) ($n_session hard -> SessionStart, $untriggered non-hard triggerless left unchanged, $unparseable unparseable derivations skipped, $skipped already had when)" >&2
372
- write_success_stamp
373
- exit 0