@indigoai-us/hq-cli 5.276.0 → 5.277.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 (77) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-pickup.sh +99 -0
  3. package/assets/scaffold/core/scripts/hq-delegate-repo.sh +158 -0
  4. package/assets/scaffold/core/scripts/hq-delegate-resolve.sh +145 -0
  5. package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +129 -0
  6. package/assets/scaffold/core/scripts/hq-delegate-transfer.sh +175 -0
  7. package/assets/scaffold/core/scripts/lib/secret-patterns.sh +58 -0
  8. package/dist/command-catalog.generated.d.ts +239 -1
  9. package/dist/command-catalog.generated.js +302 -1
  10. package/dist/commands/core-delegate.d.ts +6 -0
  11. package/dist/commands/core-delegate.js +987 -0
  12. package/dist/commands/core.js +10 -0
  13. package/dist/commands/daemon.d.ts +3 -0
  14. package/dist/commands/daemon.js +173 -1
  15. package/dist/commands/dm.d.ts +2 -0
  16. package/dist/commands/dm.js +11 -4
  17. package/dist/commands/lanes-accounts.js +44 -2
  18. package/dist/commands/lanes-message.js +69 -12
  19. package/dist/commands/lanes-resume.js +2 -0
  20. package/dist/commands/lanes-shared.d.ts +3 -0
  21. package/dist/commands/lanes-shared.js +12 -2
  22. package/dist/commands/lanes.d.ts +5 -1
  23. package/dist/commands/lanes.js +399 -47
  24. package/dist/commands/secrets.d.ts +16 -1
  25. package/dist/commands/secrets.js +77 -48
  26. package/dist/commands/sync-mode.d.ts +11 -0
  27. package/dist/commands/sync-mode.js +30 -0
  28. package/dist/fast-scaffold.d.ts +17 -0
  29. package/dist/fast-scaffold.js +91 -0
  30. package/dist/index.js +17 -1
  31. package/dist/lanes-drain-hook.js +154 -7
  32. package/dist/lib/agent-kit/run/inbox.js +3 -2
  33. package/dist/lib/checkpoint-ontology/apply.d.ts +6 -0
  34. package/dist/lib/checkpoint-ontology/apply.js +60 -3
  35. package/dist/lib/checkpoint-ontology/plan.d.ts +13 -0
  36. package/dist/lib/checkpoint-ontology/plan.js +13 -0
  37. package/dist/lib/checkpoint-ontology/tree.d.ts +2 -0
  38. package/dist/lib/checkpoint-ontology/tree.js +34 -0
  39. package/dist/lib/core-utils/jq-json-serializer.d.ts +5 -0
  40. package/dist/lib/core-utils/jq-json-serializer.js +10 -0
  41. package/dist/lib/core-utils/secret-patterns.d.ts +41 -0
  42. package/dist/lib/core-utils/secret-patterns.js +33 -0
  43. package/dist/lib/daemon/catalog.d.ts +18 -2
  44. package/dist/lib/daemon/catalog.js +18 -2
  45. package/dist/lib/daemon/config.d.ts +6 -0
  46. package/dist/lib/daemon/config.js +15 -2
  47. package/dist/lib/daemon/requests.d.ts +4 -0
  48. package/dist/lib/daemon/requests.js +2 -0
  49. package/dist/lib/daemon/run.js +94 -8
  50. package/dist/lib/daemon/supervisor.d.ts +8 -0
  51. package/dist/lib/daemon/supervisor.js +101 -1
  52. package/dist/lib/daemon/sync-runner.d.ts +58 -0
  53. package/dist/lib/daemon/sync-runner.js +277 -0
  54. package/dist/lib/doctor/__testing__/run-doctor.d.ts +8 -0
  55. package/dist/lib/doctor/__testing__/run-doctor.js +52 -0
  56. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  57. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  58. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  59. package/dist/lib/lanes/claude-balance.js +46 -8
  60. package/dist/lib/lanes/config.js +15 -0
  61. package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -0
  62. package/dist/lib/lanes/hire-grant-dispatch.js +41 -0
  63. package/dist/lib/lanes/index.d.ts +1 -1
  64. package/dist/lib/lanes/keep-alive.d.ts +3 -0
  65. package/dist/lib/lanes/keep-alive.js +3 -0
  66. package/dist/lib/lanes/outpost-dispatch.d.ts +4 -0
  67. package/dist/lib/lanes/outpost-dispatch.js +11 -2
  68. package/dist/lib/lanes/spawn.js +17 -10
  69. package/dist/lib/lanes/store.js +45 -1
  70. package/dist/lib/lanes/types.d.ts +30 -1
  71. package/dist/lib/lanes/types.js +2 -2
  72. package/dist/lib/lanes/watcher.js +3 -2
  73. package/dist/secrets-env-fast-path.d.ts +5 -0
  74. package/dist/secrets-env-fast-path.js +16 -0
  75. package/dist/secrets-env-fast-route.d.ts +18 -0
  76. package/dist/secrets-env-fast-route.js +54 -0
  77. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,82 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.277.0] — 2026-09-29
6
+
7
+ ### Fixed
8
+
9
+ - `hq lanes accounts list` and `accounts balance` now report a Claude account
10
+ whose home is missing on the current host and show the `set-home` repair
11
+ command. Balance exits non-zero for an enabled account in that state. On
12
+ Linux, account login stores a host-specific home and requires `--home` when
13
+ an existing account only has a shared home.
14
+ - Claude account balancing can use the default-off
15
+ `lanes.claude-account-balance-signals` hq-flags key to break equal live-lane
16
+ counts by the age of each account's latest completed lane run when provider
17
+ percentage data is unavailable.
18
+
19
+ ### Added
20
+
21
+ - Add a default-off `cli.secrets-env-fast-path` hq-flags route for `hq secrets
22
+ env`. It skips the full CLI graph while reusing the existing credential,
23
+ usage-audit, server authorization, and offline-cache behavior.
24
+ - `hq lanes list --json` accepts repeatable `--lane` filters, `--active`, and
25
+ owner-only hire-hierarchy snapshots. Remote lane lookup now requests the
26
+ needed hierarchy once per Outpost invocation and falls back to an unfiltered
27
+ list when an older owner CLI rejects a new filter. Internal snapshot switches
28
+ stay out of normal help and the generated command catalog.
29
+ - Add the default-off `checkpoint.ontology.dominant-sibling-link` hq-flags key.
30
+ When it is on, checkpoint ontology apply moves a knowledge link from an entity
31
+ whose name another type also holds to the same-name entity that other entity
32
+ files link to most. The move needs at least three times the inbound links and
33
+ at least five. An example is project Build Your Own AGI moving to the concept,
34
+ or the numbered copy `hq-core 2` moving to `hq-core`. Moves are reported
35
+ under `dominant_sibling_redirects`, and no entity file is written.
36
+ - Add the default-off `lanes.running-superseded-message-delivery` hq-flags key
37
+ so messages can reach otherwise messageable superseded lanes outside the done
38
+ and interrupted states. Accepted messages include a warning; done and
39
+ interrupted lanes keep the existing senior and successor checks.
40
+ - `hq core delegate resolve`, `pickup`, `repo`, `secrets`, and `transfer` are
41
+ available through native implementations when the default-off
42
+ `core-native-delegate` flag is enabled; the bundled scripts remain as the
43
+ rollback path.
44
+
45
+ - `hq lanes set <lane> --model <model> --effort <level> [--reason <text>]`
46
+ changes the model or effort a lane uses from its next resume, including the
47
+ keep-alive resume of a done lead. Before, keep-alive always resumed on the
48
+ recorded values, so a lead's model could not change without racing it. The
49
+ lane itself or its senior may run it; each change is kept in
50
+ `settings_history` and shown by `hq lanes show`.
51
+ - `hq daemon sync` reports runner ownership and health, starts an immediate sync pass, pauses or resumes sync, and sets per-company sync mode. The daemon waits for an existing desktop runner to release its lease before starting its own.
52
+ - `hq lanes interrupt <lane-id>` stops a running lane's turn and leaves it
53
+ `interrupted` (reason `operator_interrupt`) with its provider session, so
54
+ `hq lanes resume` continues it. Keep-alive does not resume it. The command
55
+ records the session id from the interrupted turn's output, holds the
56
+ provider's admission lock until the worker has stopped, and reports
57
+ `resumable: false` with a stderr warning when the turn produced no session id.
58
+ - An operator `hq lanes resume` clears keep-alive's `terminal_state` block, so
59
+ keep-alive can continue the lane after that turn.
60
+
61
+ ### Changed
62
+
63
+ - `hq lanes stop` on a running lane warns on stderr that the lane is killed and
64
+ cannot be resumed, names `hq lanes interrupt`, and adds `terminal: true` and
65
+ `resumable: false` to its JSON output, including for Outpost lanes stopped
66
+ from the primary.
67
+ - The lane drain hook avoids loading the lanes module graph when no lane is
68
+ resolved or its drop box is empty, while continuing to record tool activity
69
+ and using the full drain path whenever delivery or replay may be needed.
70
+
71
+ ### Fixed
72
+
73
+ - Doctor command tests use temporary home and state directories and skip the
74
+ live integrations inventory, so their results do not depend on a developer's
75
+ local HQ state.
76
+ - Outpost resume account moves forward the primary's hire-grant context through
77
+ the owner-side session move. The owner checks the current senior and hire
78
+ capacity before starting the worker.
79
+ - Security: `hq dm inbox --json` and agent-side raw message output omit the delegated `actingContext` object.
80
+
5
81
  ## [5.276.0] — 2026-09-28
6
82
 
7
83
  ### Added
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env bash
2
+ # hq-core: public
3
+ # hq-delegate-pickup.sh — close the delegation loop with a receipt or a FAILED
4
+ # state (2026-09-07).
5
+ #
6
+ # Why: the delegate pipeline ended at "sent", which is a send receipt, not a
7
+ # delivery receipt. Seven delegations sat at sent/building with no way to tell
8
+ # whether the recipient could act. This script adds the two missing states:
9
+ #
10
+ # sent --(evidence)--> picked-up recipient demonstrably has the work
11
+ # sent --(silence past window)--> failed nobody picked it up; sender is told why
12
+ #
13
+ # Evidence, checked in order (any one suffices):
14
+ # 1. --ack "<text>" an explicit acknowledgement (DM reply, chat) —
15
+ # recorded verbatim with a timestamp
16
+ # 2. repo branch has a commit by the recipient after sentAt
17
+ # (manifest.repo.path + repo.branch, author email == to.principal)
18
+ # 3. --evidence "<free text>" any other proof the operator observed
19
+ #
20
+ # Usage:
21
+ # hq-delegate-pickup.sh --manifest <path> --ack "<text>" # record receipt
22
+ # hq-delegate-pickup.sh --manifest <path> --evidence "<text>" # record receipt
23
+ # hq-delegate-pickup.sh --manifest <path> --check [--window-hours N] # probe repo; fail if silent past window (default 72)
24
+ # hq-delegate-pickup.sh --manifest <path> --status # print state line
25
+ #
26
+ # Exit 0: picked-up (or already). Exit 5: failed (window elapsed, no evidence).
27
+ # Exit 6: still waiting (inside window, no evidence). Exit 2: usage/precondition.
28
+ set -uo pipefail
29
+ MANIFEST=""; ACK=""; EVIDENCE=""; CHECK=0; STATUS_ONLY=0; WINDOW="${HQ_DELEGATE_PICKUP_WINDOW_HOURS:-72}"
30
+ while [ $# -gt 0 ]; do
31
+ case "$1" in
32
+ --manifest) MANIFEST="$2"; shift 2 ;;
33
+ --ack) ACK="$2"; shift 2 ;;
34
+ --evidence) EVIDENCE="$2"; shift 2 ;;
35
+ --check) CHECK=1; shift ;;
36
+ --window-hours) WINDOW="$2"; shift 2 ;;
37
+ --status) STATUS_ONLY=1; shift ;;
38
+ -h|--help) sed -n 2,28p "$0"; exit 0 ;;
39
+ *) echo "hq-delegate-pickup: unknown arg $1" >&2; exit 2 ;;
40
+ esac
41
+ done
42
+ [ -n "$MANIFEST" ] && [ -r "$MANIFEST" ] || { echo "usage: --manifest <path> (--ack|--evidence|--check|--status)" >&2; exit 2; }
43
+ command -v jq >/dev/null 2>&1 || { echo "hq-delegate-pickup: jq required" >&2; exit 2; }
44
+ NOW="${HQ_DELEGATE_NOW:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}"
45
+ STATUS="$(jq -r '.status // empty' "$MANIFEST")"
46
+ SENT_AT="$(jq -r '.sentAt // empty' "$MANIFEST")"
47
+ TO="$(jq -r '.to.principal // empty' "$MANIFEST")"
48
+ NAME="$(jq -r '.to.displayName // .to.principal // "the recipient"' "$MANIFEST")"
49
+
50
+ if [ "$STATUS_ONLY" = 1 ]; then
51
+ jq -r '"\(.delegationId) \(.status)" + (if .pickedUpAt then " picked-up " + .pickedUpAt + " via " + (.pickup.kind // "?") else "" end) + (if .failedAt then " failed " + .failedAt + ": " + (.failure.reason // "") else "" end)' "$MANIFEST"
52
+ exit 0
53
+ fi
54
+ case "$STATUS" in
55
+ picked-up) echo "hq-delegate-pickup: already picked up ($(jq -r '.pickedUpAt' "$MANIFEST"))"; exit 0 ;;
56
+ failed) echo "hq-delegate-pickup: already failed ($(jq -r '.failure.reason // ""' "$MANIFEST"))"; exit 5 ;;
57
+ sent) ;;
58
+ *) echo "hq-delegate-pickup: manifest status is '$STATUS' — only a 'sent' delegation can be picked up (send it first)" >&2; exit 2 ;;
59
+ esac
60
+
61
+ record_pickup() { # <kind> <detail>
62
+ jq --arg now "$NOW" --arg k "$1" --arg d "$2" \
63
+ '.status = "picked-up" | .pickedUpAt = $now | .pickup = {kind: $k, detail: $d, recordedAt: $now}' \
64
+ "$MANIFEST" > "$MANIFEST.tmp" && mv "$MANIFEST.tmp" "$MANIFEST"
65
+ echo "hq-delegate-pickup: $NAME picked up the delegation ($1) — status advanced to 'picked-up'"
66
+ exit 0
67
+ }
68
+ [ -n "$ACK" ] && record_pickup ack "$ACK"
69
+ [ -n "$EVIDENCE" ] && record_pickup evidence "$EVIDENCE"
70
+
71
+ [ "$CHECK" = 1 ] || { echo "hq-delegate-pickup: nothing to do (pass --ack, --evidence, --check, or --status)" >&2; exit 2; }
72
+
73
+ # Evidence 2: a commit by the recipient on the handed-over branch after sentAt.
74
+ REPO="$(jq -r '.repo.path // empty' "$MANIFEST")"
75
+ BRANCH="$(jq -r '.repo.branch // empty' "$MANIFEST")"
76
+ if [ -n "$REPO" ] && [ -d "$REPO" ] && [ -n "$BRANCH" ] && [ -n "$TO" ]; then
77
+ git -C "$REPO" fetch -q origin "$BRANCH" 2>/dev/null || true
78
+ ref="origin/$BRANCH"; git -C "$REPO" show-ref --verify --quiet "refs/remotes/$ref" || ref="$BRANCH"
79
+ c="$(git -C "$REPO" log "$ref" --author="$TO" ${SENT_AT:+--since="$SENT_AT"} --format='%h %ad %s' --date=short -1 2>/dev/null || true)"
80
+ [ -n "$c" ] && record_pickup commit "$c on $BRANCH"
81
+ fi
82
+
83
+ # No evidence. Inside the window: waiting. Past it: FAILED, with the reason.
84
+ if [ -n "$SENT_AT" ]; then
85
+ sent_epoch="$(date -u -d "$SENT_AT" +%s 2>/dev/null || date -u -j -f %Y-%m-%dT%H:%M:%SZ "$SENT_AT" +%s 2>/dev/null || echo 0)"
86
+ now_epoch="$(date -u -d "$NOW" +%s 2>/dev/null || date -u -j -f %Y-%m-%dT%H:%M:%SZ "$NOW" +%s 2>/dev/null || date +%s)"
87
+ age_h=$(( (now_epoch - sent_epoch) / 3600 ))
88
+ else
89
+ age_h=0
90
+ fi
91
+ if [ "$age_h" -lt "$WINDOW" ]; then
92
+ echo "hq-delegate-pickup: waiting — no pickup evidence from $NAME yet (${age_h}h of a ${WINDOW}h window)"
93
+ exit 6
94
+ fi
95
+ reason="no pickup evidence from $NAME within ${WINDOW}h of send (${SENT_AT:-unknown}): no acknowledgement, no commit by $TO on ${BRANCH:-the branch}"
96
+ jq --arg now "$NOW" --arg r "$reason" '.status = "failed" | .failedAt = $now | .failure = {reason: $r, recordedAt: $now}' \
97
+ "$MANIFEST" > "$MANIFEST.tmp" && mv "$MANIFEST.tmp" "$MANIFEST"
98
+ echo "hq-delegate-pickup: FAILED — $reason. Re-send with a direct ask, or hand it to someone else." >&2
99
+ exit 5
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env bash
2
+ # hq-core: public
3
+ # hq-delegate-repo.sh — code/branch handover for a delegation: make sure the
4
+ # project's branch exists on the remote, record uncommitted work as NOT
5
+ # transferred, and verify the recipient's repository access read-only.
6
+ #
7
+ # Usage:
8
+ # core/scripts/hq-delegate-repo.sh --manifest <path> [--yes] [--github-user <login>]
9
+ #
10
+ # Behavior (driven by the manifest's repo{} block; no-op when repo is null):
11
+ # - records remote URL and the branch's headSha into the manifest
12
+ # - a branch that exists only locally requires confirmation: without --yes
13
+ # the helper prints the plan and exits 2; with --yes it pushes via an
14
+ # explicitly anchored `git -C <abs path> push -u origin <branch>`
15
+ # - dirty/untracked files are captured into repo.dirtyFiles[] and appended
16
+ # to BRIEF.md as work NOT transferred — never silently dropped
17
+ # - recipient access is checked read-only via gh when --github-user is
18
+ # given; otherwise (or on a gh failure) repo.accessVerified stays false
19
+ # and the brief says so plainly — the helper never claims unverified access
20
+ # - never touches vaultPrefixes[] and never pushes repo content to the vault
21
+ #
22
+ # Env: HQ_ROOT — HQ root override (default: resolved from script location)
23
+
24
+ set -euo pipefail
25
+
26
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
27
+ HQ_ROOT="${HQ_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
28
+
29
+ usage() { sed -n '3,22p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
30
+ die() { echo "hq-delegate-repo: $*" >&2; exit 1; }
31
+
32
+ command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
33
+
34
+ MANIFEST="" YES=0 GH_USER=""
35
+ while [ $# -gt 0 ]; do
36
+ case "$1" in
37
+ --manifest) MANIFEST="${2:-}"; shift 2 ;;
38
+ --yes) YES=1; shift ;;
39
+ --github-user) GH_USER="${2:-}"; shift 2 ;;
40
+ -h|--help) usage; exit 0 ;;
41
+ *) die "unknown argument: $1" ;;
42
+ esac
43
+ done
44
+
45
+ [ -n "$MANIFEST" ] || die "--manifest is required"
46
+ [ -f "$MANIFEST" ] || die "manifest not found: $MANIFEST"
47
+ jq -e . "$MANIFEST" >/dev/null 2>&1 || die "manifest is not valid JSON: $MANIFEST"
48
+
49
+ BUNDLE_DIR="$(cd "$(dirname "$MANIFEST")" && pwd)"
50
+ BRIEF="$BUNDLE_DIR/BRIEF.md"
51
+
52
+ # --- no-repo projects skip cleanly -------------------------------------------
53
+
54
+ if jq -e '.repo == null' "$MANIFEST" >/dev/null; then
55
+ echo "hq-delegate-repo: project has no repoPath — nothing to hand over"
56
+ exit 0
57
+ fi
58
+
59
+ REPO_REL="$(jq -r '.repo.path // empty' "$MANIFEST")"
60
+ BRANCH="$(jq -r '.repo.branch // empty' "$MANIFEST")"
61
+ [ -n "$REPO_REL" ] || die "manifest repo block has no path"
62
+
63
+ REPO_DIR="$HQ_ROOT/$REPO_REL"
64
+ [ -d "$REPO_DIR/.git" ] || git -C "$REPO_DIR" rev-parse --git-dir >/dev/null 2>&1 \
65
+ || die "not a git repository: $REPO_DIR"
66
+
67
+ REMOTE_URL="$(git -C "$REPO_DIR" remote get-url origin 2>/dev/null || true)"
68
+
69
+ # --- dirty / untracked work is recorded, never dropped -----------------------
70
+
71
+ DIRTY_JSON="$(git -C "$REPO_DIR" status --porcelain | sed 's/^...//' | jq -R . | jq -cs .)"
72
+
73
+ # --- branch state -------------------------------------------------------------
74
+
75
+ BRANCH_PUSHED=false HEAD_SHA=""
76
+ if [ -n "$BRANCH" ]; then
77
+ git check-ref-format --branch "$BRANCH" >/dev/null 2>&1 || die "invalid branch name in PRD: $BRANCH"
78
+ LOCAL_SHA="$(git -C "$REPO_DIR" rev-parse --verify --quiet "refs/heads/$BRANCH" || true)"
79
+ REMOTE_SHA="$(git -C "$REPO_DIR" rev-parse --verify --quiet "refs/remotes/origin/$BRANCH" || true)"
80
+
81
+ if [ -n "$REMOTE_SHA" ]; then
82
+ BRANCH_PUSHED=true
83
+ HEAD_SHA="$REMOTE_SHA"
84
+ elif [ -n "$LOCAL_SHA" ]; then
85
+ echo "Branch handover plan — repo '$REPO_REL':"
86
+ echo " branch '$BRANCH' exists only locally ($LOCAL_SHA)"
87
+ echo " action: git -C $REPO_DIR push -u origin $BRANCH"
88
+ if [ "$YES" -ne 1 ]; then
89
+ echo
90
+ echo "hq-delegate-repo: confirmation required — re-run with --yes to push the branch" >&2
91
+ exit 2
92
+ fi
93
+ git -C "$REPO_DIR" push -u origin "$BRANCH" \
94
+ || die "failed to push branch '$BRANCH' to origin"
95
+ BRANCH_PUSHED=true
96
+ HEAD_SHA="$LOCAL_SHA"
97
+ else
98
+ echo "hq-delegate-repo: branch '$BRANCH' does not exist yet (declared in the PRD, not started) — recording as unpushed"
99
+ fi
100
+ fi
101
+
102
+ # --- read-only recipient access check ----------------------------------------
103
+
104
+ ACCESS_VERIFIED=false ACCESS_NOTE=""
105
+ if [ -n "$GH_USER" ] && [ -n "$REMOTE_URL" ] && command -v gh >/dev/null 2>&1; then
106
+ # owner/repo from ssh or https remote forms
107
+ OWNER_REPO="$(printf '%s' "$REMOTE_URL" | sed -E 's#^(git@[^:]+:|https?://[^/]+/)##; s#\.git$##')"
108
+ if gh api "repos/$OWNER_REPO/collaborators/$GH_USER" >/dev/null 2>&1; then
109
+ ACCESS_VERIFIED=true
110
+ ACCESS_NOTE="verified: $GH_USER is a collaborator on $OWNER_REPO"
111
+ else
112
+ ACCESS_NOTE="UNVERIFIED: could not confirm $GH_USER has access to $OWNER_REPO — the recipient may need to be invited before they can pull the branch"
113
+ fi
114
+ else
115
+ ACCESS_NOTE="UNVERIFIED: recipient repository access was not checked (no GitHub login supplied) — confirm they can reach the repo before assuming so"
116
+ fi
117
+
118
+ # --- manifest update (repo block only — vaultPrefixes are never touched) -----
119
+
120
+ TMP_MANIFEST="$(mktemp)"
121
+ jq \
122
+ --arg remote "$REMOTE_URL" \
123
+ --arg headSha "$HEAD_SHA" \
124
+ --argjson dirty "$DIRTY_JSON" \
125
+ --argjson pushed "$BRANCH_PUSHED" \
126
+ --argjson access "$ACCESS_VERIFIED" \
127
+ --arg accessNote "$ACCESS_NOTE" \
128
+ '.repo.remote = (if $remote == "" then null else $remote end)
129
+ | .repo.headSha = (if $headSha == "" then null else $headSha end)
130
+ | .repo.dirtyFiles = $dirty
131
+ | .repo.branchPushed = $pushed
132
+ | .repo.accessVerified = $access
133
+ | .repo.accessNote = $accessNote' \
134
+ "$MANIFEST" > "$TMP_MANIFEST" && mv "$TMP_MANIFEST" "$MANIFEST"
135
+
136
+ # --- brief: call out untransferred work and unverified access ----------------
137
+
138
+ if [ -f "$BRIEF" ]; then
139
+ DIRTY_COUNT="$(printf '%s' "$DIRTY_JSON" | jq 'length')"
140
+ if [ "$DIRTY_COUNT" -gt 0 ] || [ "$ACCESS_VERIFIED" != "true" ]; then
141
+ {
142
+ echo
143
+ echo "## Code handover notes"
144
+ echo
145
+ if [ "$DIRTY_COUNT" -gt 0 ]; then
146
+ echo "The delegator's working tree had uncommitted changes that are **not transferred** with this delegation. If something seems missing, these files are why — ask the delegator to commit and push them:"
147
+ echo
148
+ printf '%s' "$DIRTY_JSON" | jq -r '.[] | "- `\(.)`"'
149
+ echo
150
+ fi
151
+ if [ "$ACCESS_VERIFIED" != "true" ]; then
152
+ echo "$ACCESS_NOTE"
153
+ fi
154
+ } >> "$BRIEF"
155
+ fi
156
+ fi
157
+
158
+ echo "hq-delegate-repo: recorded branch state (pushed=$BRANCH_PUSHED, dirty=$(printf '%s' "$DIRTY_JSON" | jq 'length') files, accessVerified=$ACCESS_VERIFIED)"
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env bash
2
+ # hq-core: public
3
+ # hq-delegate-resolve.sh — resolve a delegation recipient to a confirmed
4
+ # principal (person email / prs_ uid, or fleet-agent agt_ uid). Read-only.
5
+ #
6
+ # Usage:
7
+ # core/scripts/hq-delegate-resolve.sh --company <slug> --to <token>
8
+ #
9
+ # Output (stdout, JSON): {kind, principal, displayName, source}
10
+ # kind — "person" | "agent"
11
+ # principal — confirmed email, prs_… or agt_… uid
12
+ # source — "verbatim" | "people-resolve" | "agents-list"
13
+ #
14
+ # Exit codes:
15
+ # 0 — resolved
16
+ # 1 — usage error (missing/invalid arguments)
17
+ # 3 — ambiguous: candidate list printed to stdout as JSON
18
+ # {status:"ambiguous", matches:[…]} — caller must present a picker
19
+ # 4 — not found (or found with no email): plain message on stderr
20
+ #
21
+ # Tenancy: every lookup is scoped with an explicit --company. This helper
22
+ # never enumerates or falls back across companies — single-company by
23
+ # construction, matching the /dm skill's resolution contract.
24
+ #
25
+ # Fast path: a token containing "@" or starting with prs_ / agt_ passes
26
+ # through verbatim with NO lookup, preserving existing invocations exactly.
27
+
28
+ set -euo pipefail
29
+
30
+ usage() {
31
+ sed -n '3,26p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
32
+ }
33
+
34
+ die_usage() {
35
+ echo "hq-delegate-resolve: $*" >&2
36
+ usage >&2
37
+ exit 1
38
+ }
39
+
40
+ command -v jq >/dev/null 2>&1 || { echo "hq-delegate-resolve: jq is required" >&2; exit 1; }
41
+
42
+ COMPANY="" TOKEN=""
43
+ while [ $# -gt 0 ]; do
44
+ case "$1" in
45
+ --company) COMPANY="${2:-}"; shift 2 ;;
46
+ --to) TOKEN="${2:-}"; shift 2 ;;
47
+ -h|--help) usage; exit 0 ;;
48
+ *) die_usage "unknown argument: $1" ;;
49
+ esac
50
+ done
51
+
52
+ [ -n "$COMPANY" ] || die_usage "--company is required (single-company lookups only; never a multi-company scan)"
53
+ [ -n "$TOKEN" ] || die_usage "--to is required"
54
+
55
+ emit() { # kind principal displayName source
56
+ jq -cn --arg kind "$1" --arg principal "$2" --arg name "$3" --arg source "$4" \
57
+ '{kind: $kind, principal: $principal,
58
+ displayName: (if $name == "" then null else $name end), source: $source}'
59
+ }
60
+
61
+ # Strip CLI noise (version warnings, session-refresh notices) before the JSON
62
+ # payload: keep everything from the first line that starts a JSON value.
63
+ json_only() {
64
+ awk 'started {print; next} /^[[:space:]]*[{[]/ {started=1; print}'
65
+ }
66
+
67
+ # --- fast path: already a principal — pass through verbatim, no lookup -------
68
+
69
+ case "$TOKEN" in
70
+ agt_*)
71
+ emit agent "$TOKEN" "" verbatim
72
+ exit 0
73
+ ;;
74
+ prs_*|*@*)
75
+ emit person "$TOKEN" "" verbatim
76
+ exit 0
77
+ ;;
78
+ esac
79
+
80
+ # --- name → people roster (single-company, read-only) ------------------------
81
+
82
+ PEOPLE_RAW="$(hq people resolve "$TOKEN" --json --company "$COMPANY" 2>/dev/null | json_only || true)"
83
+ PEOPLE_STATUS=""
84
+ if [ -n "$PEOPLE_RAW" ] && printf '%s' "$PEOPLE_RAW" | jq -e . >/dev/null 2>&1; then
85
+ PEOPLE_STATUS="$(printf '%s' "$PEOPLE_RAW" | jq -r '.status // empty')"
86
+ fi
87
+
88
+ case "$PEOPLE_STATUS" in
89
+ found)
90
+ EMAIL="$(printf '%s' "$PEOPLE_RAW" | jq -r '.email // empty')"
91
+ NAME="$(printf '%s' "$PEOPLE_RAW" | jq -r '.name // .displayName // empty')"
92
+ [ -n "$EMAIL" ] || { echo "hq-delegate-resolve: resolver returned found but no email for '$TOKEN'" >&2; exit 4; }
93
+ emit person "$EMAIL" "$NAME" people-resolve
94
+ exit 0
95
+ ;;
96
+ ambiguous)
97
+ printf '%s' "$PEOPLE_RAW" | jq -c '{status: "ambiguous", matches: (.matches // [])}'
98
+ exit 3
99
+ ;;
100
+ no_email)
101
+ # A person entry without an email is often a fleet agent's roster row —
102
+ # fall through to the agent roster before treating this as terminal.
103
+ PERSON_NO_EMAIL=1
104
+ ;;
105
+ not_found|"")
106
+ : # fall through to the fleet-agent roster
107
+ ;;
108
+ *)
109
+ echo "hq-delegate-resolve: unexpected resolver status '$PEOPLE_STATUS' for '$TOKEN'" >&2
110
+ exit 4
111
+ ;;
112
+ esac
113
+
114
+ # --- name → fleet-agent roster (same company, read-only) ---------------------
115
+
116
+ AGENTS_RAW="$(hq agents list --company "$COMPANY" --json 2>/dev/null | json_only || true)"
117
+ if [ -z "$AGENTS_RAW" ] || ! printf '%s' "$AGENTS_RAW" | jq -e . >/dev/null 2>&1; then
118
+ AGENTS_RAW="[]"
119
+ fi
120
+
121
+ MATCHES="$(printf '%s' "$AGENTS_RAW" | jq -c --arg t "$TOKEN" '
122
+ [ .[]
123
+ | select((.name // .displayName // "") | ascii_downcase == ($t | ascii_downcase))
124
+ | {agentUid: (.agentUid // .uid // empty), name: (.name // .displayName // "")}
125
+ | select(.agentUid != "")
126
+ ]')"
127
+ MATCH_COUNT="$(printf '%s' "$MATCHES" | jq 'length')"
128
+
129
+ if [ "$MATCH_COUNT" -eq 1 ]; then
130
+ emit agent \
131
+ "$(printf '%s' "$MATCHES" | jq -r '.[0].agentUid')" \
132
+ "$(printf '%s' "$MATCHES" | jq -r '.[0].name')" \
133
+ agents-list
134
+ exit 0
135
+ elif [ "$MATCH_COUNT" -gt 1 ]; then
136
+ printf '%s' "$MATCHES" | jq -c '{status: "ambiguous", matches: .}'
137
+ exit 3
138
+ fi
139
+
140
+ if [ "${PERSON_NO_EMAIL:-0}" -eq 1 ]; then
141
+ echo "hq-delegate-resolve: '$TOKEN' was found in company '$COMPANY' but has no email on record and matches no fleet agent — pass their exact email, personUid (prs_…), or agentUid (agt_…) instead" >&2
142
+ else
143
+ echo "hq-delegate-resolve: no teammate or fleet agent named '$TOKEN' found in company '$COMPANY' — pass the exact email, personUid (prs_…), or agentUid (agt_…) instead" >&2
144
+ fi
145
+ exit 4
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env bash
2
+ # hq-core: public
3
+ # hq-delegate-secrets.sh — grant a delegation recipient access to the secrets
4
+ # a project needs, BY NAME ONLY. No command in this helper ever reads,
5
+ # prints, logs, or interpolates a secret value.
6
+ #
7
+ # Usage:
8
+ # core/scripts/hq-delegate-secrets.sh --manifest <path> [--yes] [--no-secrets]
9
+ # [--env-schema <path>]
10
+ #
11
+ # Behavior:
12
+ # - derives required secret NAMES from the project's / repo's .env.schema
13
+ # (--env-schema overrides discovery); no schema -> records secrets: []
14
+ # and says so plainly rather than guessing at credentials
15
+ # - without --yes: prints a full-prose confirmation listing every name and
16
+ # the principal, then exits 2 granting nothing — this step is a privilege
17
+ # handover and gets its own confirmation, separate from the file grants
18
+ # - with --yes: hq secrets share <NAME> --with <principal> --permission read
19
+ # per name, then records the names (and only the names) in the manifest
20
+ # - --no-secrets: skips everything, records {secrets: [], secretsSkipped: true}
21
+ #
22
+ # Consumption on the recipient's side is via `hq run` / `hq secrets exec` —
23
+ # documented in the brief; values never appear in any delegation artifact
24
+ # (policy hq-delegate-never-inlines-secrets-or-share-urls).
25
+ #
26
+ # Env: HQ_ROOT — HQ root override (default: resolved from script location)
27
+
28
+ set -euo pipefail
29
+
30
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
31
+ HQ_ROOT="${HQ_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
32
+
33
+ usage() { sed -n '3,24p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
34
+ die() { echo "hq-delegate-secrets: $*" >&2; exit 1; }
35
+
36
+ command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
37
+
38
+ MANIFEST="" YES=0 NO_SECRETS=0 ENV_SCHEMA=""
39
+ while [ $# -gt 0 ]; do
40
+ case "$1" in
41
+ --manifest) MANIFEST="${2:-}"; shift 2 ;;
42
+ --yes) YES=1; shift ;;
43
+ --no-secrets) NO_SECRETS=1; shift ;;
44
+ --env-schema) ENV_SCHEMA="${2:-}"; shift 2 ;;
45
+ -h|--help) usage; exit 0 ;;
46
+ *) die "unknown argument: $1" ;;
47
+ esac
48
+ done
49
+
50
+ [ -n "$MANIFEST" ] || die "--manifest is required"
51
+ [ -f "$MANIFEST" ] || die "manifest not found: $MANIFEST"
52
+ jq -e . "$MANIFEST" >/dev/null 2>&1 || die "manifest is not valid JSON: $MANIFEST"
53
+
54
+ COMPANY="$(jq -r '.company // empty' "$MANIFEST")"
55
+ PRINCIPAL="$(jq -r '.to.principal // empty' "$MANIFEST")"
56
+ PROJECT="$(jq -r '.project.name // empty' "$MANIFEST")"
57
+ [ -n "$COMPANY" ] || die "manifest has no company"
58
+ [ -n "$PRINCIPAL" ] || die "manifest has no to.principal"
59
+
60
+ update_manifest() { # jq-filter
61
+ local tmp
62
+ tmp="$(mktemp)"
63
+ jq "$1" "$MANIFEST" > "$tmp" && mv "$tmp" "$MANIFEST"
64
+ }
65
+
66
+ # --- explicit skip ------------------------------------------------------------
67
+
68
+ if [ "$NO_SECRETS" -eq 1 ]; then
69
+ update_manifest '.secrets = [] | .secretsSkipped = true'
70
+ echo "hq-delegate-secrets: skipped (--no-secrets) — no credential access granted"
71
+ exit 0
72
+ fi
73
+
74
+ # --- discover the env schema --------------------------------------------------
75
+
76
+ if [ -z "$ENV_SCHEMA" ]; then
77
+ REPO_REL="$(jq -r '.repo.path // empty' "$MANIFEST")"
78
+ for candidate in \
79
+ "$HQ_ROOT/companies/$COMPANY/projects/$PROJECT/.env.schema" \
80
+ "${REPO_REL:+$HQ_ROOT/$REPO_REL/.env.schema}"; do
81
+ [ -n "$candidate" ] && [ -f "$candidate" ] && { ENV_SCHEMA="$candidate"; break; }
82
+ done
83
+ fi
84
+
85
+ if [ -z "$ENV_SCHEMA" ] || [ ! -f "$ENV_SCHEMA" ]; then
86
+ update_manifest '.secrets = [] | .secretsSkipped = false'
87
+ echo "hq-delegate-secrets: no .env.schema found for this project or its repo — no secret names to grant. If the project needs credentials, add an .env.schema and re-run, or grant them manually with 'hq secrets share'."
88
+ exit 0
89
+ fi
90
+
91
+ # --- parse names (KEY= lines; comments and blanks ignored) --------------------
92
+
93
+ NAMES="$(grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$ENV_SCHEMA" | cut -d= -f1 | sort -u)"
94
+ if [ -z "$NAMES" ]; then
95
+ update_manifest '.secrets = [] | .secretsSkipped = false'
96
+ echo "hq-delegate-secrets: $ENV_SCHEMA declares no secret names — nothing to grant"
97
+ exit 0
98
+ fi
99
+ NAME_COUNT="$(printf '%s\n' "$NAMES" | wc -l | tr -d ' ')"
100
+
101
+ # --- confirmation gate (full prose; separate from the file-grant confirm) ----
102
+
103
+ echo "Secret access handover — this grants the recipient the ability to use these credentials."
104
+ echo
105
+ echo "The following $NAME_COUNT secret name(s), declared by $(basename "$ENV_SCHEMA") for project '$PROJECT', will be shared with read permission to '$PRINCIPAL' in company '$COMPANY':"
106
+ echo
107
+ printf '%s\n' "$NAMES" | sed 's/^/ - /'
108
+ echo
109
+ echo "Only the names are granted and recorded; no secret value is read, printed, or transmitted by this step. The recipient consumes them through 'hq run' / 'hq secrets exec'."
110
+
111
+ if [ "$YES" -ne 1 ]; then
112
+ echo
113
+ echo "hq-delegate-secrets: confirmation required — re-run with --yes after the user approves (or --no-secrets to skip)" >&2
114
+ exit 2
115
+ fi
116
+
117
+ # --- grant each name (names only — never a value read) ------------------------
118
+
119
+ printf '%s\n' "$NAMES" | while IFS= read -r name; do
120
+ hq secrets share "$name" --with "$PRINCIPAL" --permission read --company "$COMPANY" \
121
+ || die "failed to grant '$name' to '$PRINCIPAL' — aborting; manifest not updated"
122
+ echo "hq-delegate-secrets: granted read on '$name' to '$PRINCIPAL'"
123
+ done
124
+
125
+ NAMES_JSON="$(printf '%s\n' "$NAMES" | jq -R . | jq -cs .)"
126
+ NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
127
+ update_manifest ".secrets = $NAMES_JSON | .secretsSkipped = false | .secretsGrantedAt = \"$NOW\""
128
+
129
+ echo "hq-delegate-secrets: $NAME_COUNT secret name(s) granted and recorded (names only)"