@indigoai-us/hq-cli 5.290.0 → 5.292.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 (83) hide show
  1. package/CHANGELOG.md +66 -3
  2. package/assets/scaffold/core/scripts/handoff-finalize.sh +18 -0
  3. package/assets/scaffold/core/scripts/handoff-post.sh +71 -6
  4. package/assets/scaffold/core/scripts/hook-lib.sh +345 -50
  5. package/assets/scaffold/core/scripts/hq-delegate-bundle.sh +307 -0
  6. package/assets/scaffold/core/scripts/hq-delegate-grant.sh +226 -0
  7. package/assets/scaffold/core/scripts/hq-delegate-publish.sh +91 -0
  8. package/assets/scaffold/core/scripts/hq-delegate-send.sh +259 -0
  9. package/assets/scaffold/core/scripts/hq-delegate-verify.sh +213 -0
  10. package/assets/scaffold/core/scripts/hq-job-notify.sh +389 -0
  11. package/assets/scaffold/core/scripts/hq-job-probe.sh +835 -0
  12. package/assets/scaffold/core/scripts/hq-job-remediate.sh +359 -0
  13. package/assets/scaffold/core/scripts/hq-job-run.sh +1025 -0
  14. package/assets/scaffold/core/scripts/hq-session.sh +182 -21
  15. package/assets/scaffold/core/scripts/jobs-schedule-parse.sh +310 -0
  16. package/assets/scaffold/core/scripts/jobs-validate.sh +451 -0
  17. package/assets/scaffold/core/scripts/lib/session-id.sh +3 -2
  18. package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +2 -1
  19. package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +830 -0
  20. package/dist/command-catalog.generated.d.ts +165 -1
  21. package/dist/command-catalog.generated.js +205 -1
  22. package/dist/commands/cloud.d.ts +26 -14
  23. package/dist/commands/cloud.js +207 -56
  24. package/dist/commands/core-delegate-w1b.d.ts +8 -0
  25. package/dist/commands/core-delegate-w1b.js +1049 -0
  26. package/dist/commands/core-delegate.d.ts +28 -0
  27. package/dist/commands/core-delegate.js +15 -13
  28. package/dist/commands/core-handoff.js +152 -23
  29. package/dist/commands/core-jobs-native.d.ts +23 -0
  30. package/dist/commands/core-jobs-native.js +1331 -0
  31. package/dist/commands/core-native-jobs-flag.d.ts +4 -0
  32. package/dist/commands/core-native-jobs-flag.js +7 -0
  33. package/dist/commands/core-ontology.d.ts +22 -4
  34. package/dist/commands/core-ontology.js +90 -8
  35. package/dist/commands/core-policy.js +24 -5
  36. package/dist/commands/core.js +17 -0
  37. package/dist/commands/integrations-core.js +7 -0
  38. package/dist/commands/lanes.js +111 -35
  39. package/dist/commands/members.js +5 -0
  40. package/dist/commands/mesh.js +46 -25
  41. package/dist/commands/personal-targeted-push-flag.d.ts +4 -0
  42. package/dist/commands/personal-targeted-push-flag.js +7 -0
  43. package/dist/commands/personal-targeted-push-paths.d.ts +3 -0
  44. package/dist/commands/personal-targeted-push-paths.js +67 -0
  45. package/dist/commands/secrets.js +9 -0
  46. package/dist/commands/skill.d.ts +3 -0
  47. package/dist/commands/skill.js +28 -0
  48. package/dist/fast-core.js +1 -0
  49. package/dist/lib/billing/free-month.d.ts +18 -0
  50. package/dist/lib/billing/free-month.js +31 -0
  51. package/dist/lib/billing/plan-limit-refusals.d.ts +69 -0
  52. package/dist/lib/billing/plan-limit-refusals.js +187 -0
  53. package/dist/lib/billing/plan-lock.d.ts +2 -6
  54. package/dist/lib/billing/plan-lock.js +5 -16
  55. package/dist/lib/checkpoint-ontology/drop-aliases.d.ts +41 -0
  56. package/dist/lib/checkpoint-ontology/drop-aliases.js +76 -0
  57. package/dist/lib/checkpoint-ontology/entity-file.d.ts +6 -3
  58. package/dist/lib/checkpoint-ontology/entity-file.js +12 -6
  59. package/dist/lib/checkpoint-ontology/merge-copies.d.ts +27 -8
  60. package/dist/lib/checkpoint-ontology/merge-copies.js +64 -97
  61. package/dist/lib/core-utils/secret-patterns.d.ts +3 -1
  62. package/dist/lib/core-utils/secret-patterns.js +3 -2
  63. package/dist/lib/daemon/flag.d.ts +2 -0
  64. package/dist/lib/daemon/flag.js +2 -0
  65. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  66. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  67. package/dist/lib/lanes/autopilot.d.ts +69 -0
  68. package/dist/lib/lanes/autopilot.js +418 -0
  69. package/dist/lib/lanes/mesh-events.js +2 -0
  70. package/dist/lib/lanes/store.js +11 -6
  71. package/dist/lib/mesh/api.js +4 -0
  72. package/dist/lib/mesh/live/daemon/state.d.ts +7 -1
  73. package/dist/lib/mesh/live/daemon/state.js +34 -2
  74. package/dist/main.js +23 -12
  75. package/dist/utils/plan-gate-error.d.ts +24 -0
  76. package/dist/utils/plan-gate-error.js +219 -9
  77. package/dist/utils/scaffold-sibling-references.js +4 -4
  78. package/dist/utils/self-update.d.ts +6 -0
  79. package/dist/utils/self-update.js +28 -6
  80. package/dist/utils/staged-install.d.ts +34 -0
  81. package/dist/utils/staged-install.js +296 -0
  82. package/package.json +2 -2
  83. package/scripts/mesh-daemon-if-stale.mjs +4 -0
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env bash
2
+ # hq-core: public
3
+ # hq-delegate-send.sh — generate the self-sufficient pickup prompt for a
4
+ # delegation and deliver it by DM. The prompt IS the handoff: pasted into a
5
+ # fresh agent session on a clean HQ install, it pulls every file on demand
6
+ # (hq files get per prefix — no /hq-sync run) and reaches the first
7
+ # next-step with no follow-up question to the delegator.
8
+ #
9
+ # Usage:
10
+ # core/scripts/hq-delegate-send.sh --manifest <path> [--send]
11
+ # [--headline <text>] [--note <text>]
12
+ #
13
+ # Without --send: (re)generates PICKUP-PROMPT.md next to the manifest and
14
+ # prints its path — inspectable, idempotent, sends nothing.
15
+ # With --send: requires manifest status "verified" (the US-008 probe must
16
+ # have passed), then delivers ONE `hq dm` with --prompt-file/--details-file
17
+ # and advances status to "sent", recording the DM eventId.
18
+ #
19
+ # The caller (the /delegate skill) humanizes --headline / --note per
20
+ # core/knowledge/public/hq-core/humanize-before-send.md (channel dm, light)
21
+ # BEFORE invoking; this helper never rewrites recipients, flags, or the
22
+ # generated command lines.
23
+ #
24
+ # Fail-closed: the generated prompt is scanned for secret-shaped content and
25
+ # share-session URLs; a match aborts before anything is written or sent
26
+ # (policy hq-delegate-never-inlines-secrets-or-share-urls).
27
+ #
28
+ # Env: HQ_ROOT — HQ root override (default: resolved from script location)
29
+
30
+ set -euo pipefail
31
+
32
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
33
+ HQ_ROOT="${HQ_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
34
+
35
+ # shellcheck source=lib/secret-patterns.sh
36
+ . "$SCRIPT_DIR/lib/secret-patterns.sh"
37
+
38
+ usage() { sed -n '3,27p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
39
+ die() { echo "hq-delegate-send: $*" >&2; exit 1; }
40
+
41
+ command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
42
+
43
+ MANIFEST="" SEND=0 HEADLINE="" NOTE=""
44
+ while [ $# -gt 0 ]; do
45
+ case "$1" in
46
+ --manifest) MANIFEST="${2:-}"; shift 2 ;;
47
+ --send) SEND=1; shift ;;
48
+ --headline) HEADLINE="${2:-}"; shift 2 ;;
49
+ --note) NOTE="${2:-}"; shift 2 ;;
50
+ -h|--help) usage; exit 0 ;;
51
+ *) die "unknown argument: $1" ;;
52
+ esac
53
+ done
54
+
55
+ [ -n "$MANIFEST" ] || die "--manifest is required"
56
+ [ -f "$MANIFEST" ] || die "manifest not found: $MANIFEST"
57
+ jq -e . "$MANIFEST" >/dev/null 2>&1 || die "manifest is not valid JSON: $MANIFEST"
58
+ LOCK="$MANIFEST.send-lock"
59
+ if [ "$SEND" -eq 1 ]; then
60
+ mkdir "$LOCK" 2>/dev/null || die "verification or delivery is active; retry after it finishes"
61
+ trap 'rmdir "$LOCK"' EXIT
62
+ fi
63
+
64
+ BUNDLE_DIR="$(cd "$(dirname "$MANIFEST")" && pwd)"
65
+ BRIEF="$BUNDLE_DIR/BRIEF.md"
66
+ PROMPT="$BUNDLE_DIR/PICKUP-PROMPT.md"
67
+
68
+ COMPANY="$(jq -r '.company // empty' "$MANIFEST")"
69
+ PROJECT="$(jq -r '.project.name // empty' "$MANIFEST")"
70
+ PRINCIPAL="$(jq -r '.to.principal // empty' "$MANIFEST")"
71
+ DISPLAY="$(jq -r '.to.displayName // .to.principal' "$MANIFEST")"
72
+ FROM="$(jq -r '.from.email // .from.personUid // "your teammate"' "$MANIFEST")"
73
+ DELEGATION_ID="$(jq -r '.delegationId // empty' "$MANIFEST")"
74
+ STATUS="$(jq -r '.status // empty' "$MANIFEST")"
75
+ MODE="$(jq -r '.mode // "transfer"' "$MANIFEST")"
76
+ [ -n "$COMPANY" ] || die "manifest has no company"
77
+ [ -n "$PROJECT" ] || die "manifest has no project.name"
78
+ [ -n "$PRINCIPAL" ] || die "manifest has no to.principal"
79
+
80
+ PRD_ABS="$HQ_ROOT/$(jq -r '.project.prdPath' "$MANIFEST")"
81
+
82
+ # --- assemble prompt sections from the manifest (single source of truth) -----
83
+
84
+ GOAL="" STATE_LINE="" NEXT_STEPS=""
85
+ if [ -f "$PRD_ABS" ] && jq -e . "$PRD_ABS" >/dev/null 2>&1; then
86
+ GOAL="$(jq -r '.metadata.goal // .description // ""' "$PRD_ABS")"
87
+ TOTAL="$(jq -r '[.userStories // [] | .[]] | length' "$PRD_ABS")"
88
+ DONE="$(jq -r '[.userStories // [] | .[] | select(.passes == true)] | length' "$PRD_ABS")"
89
+ STATE_LINE="$DONE of $TOTAL stories are complete."
90
+ NEXT_STEPS="$(jq -r '[.userStories // [] | .[] | select(.passes != true)]
91
+ | sort_by(.priority) | .[:3] | to_entries | .[]
92
+ | "\(.key + 1). **\(.value.id): \(.value.title)** — \(.value.description)"' "$PRD_ABS")"
93
+ fi
94
+
95
+ GET_LINES="$(jq -r --arg co "$COMPANY" \
96
+ '.vaultPrefixes[] | "hq files get \(.prefix) --company \($co)"' "$MANIFEST")"
97
+
98
+ REPO_IS_NULL="$(jq -r 'if .repo == null then "yes" else "no" end' "$MANIFEST")"
99
+ REPO_SECTION=""
100
+ if [ "$REPO_IS_NULL" = "no" ]; then
101
+ R_PATH="$(jq -r '.repo.path // empty' "$MANIFEST")"
102
+ R_REMOTE="$(jq -r '.repo.remote // empty' "$MANIFEST")"
103
+ R_BRANCH="$(jq -r '.repo.branch // empty' "$MANIFEST")"
104
+ R_BASE="$(jq -r '.repo.baseBranch // "main"' "$MANIFEST")"
105
+ R_SHA="$(jq -r '.repo.headSha // empty' "$MANIFEST")"
106
+ R_ACCESS="$(jq -r '.repo.accessNote // empty' "$MANIFEST")"
107
+ if [ -n "$R_BRANCH" ] && git check-ref-format --branch "$R_BRANCH" >/dev/null 2>&1 && [ -n "$R_SHA" ]; then
108
+ REPO_SECTION="## Get the code
109
+
110
+ The work lives on branch \`$R_BRANCH\` (base: \`$R_BASE\`)${R_SHA:+ at \`$R_SHA\`}.
111
+
112
+ \`\`\`bash
113
+ # From your HQ root; clone first if the repo is not present${R_REMOTE:+ (remote: $R_REMOTE)}:
114
+ git -C $(printf '%q' "$R_PATH") fetch origin
115
+ git -C $(printf '%q' "$R_PATH") switch -- $(printf '%q' "$R_BRANCH")
116
+ \`\`\`
117
+ ${R_ACCESS:+
118
+ > $R_ACCESS
119
+ }"
120
+ fi
121
+ fi
122
+
123
+ SECRET_NAMES="$(jq -r '[.secrets // [] | .[]] | join(",")' "$MANIFEST")"
124
+ SECRETS_SECTION=""
125
+ if [ -n "$SECRET_NAMES" ]; then
126
+ SECRETS_SECTION="## Credentials
127
+
128
+ You have been granted read access to the secret names this project needs: \`$SECRET_NAMES\`. Values never travel in this handoff — consume them at runtime:
129
+
130
+ \`\`\`bash
131
+ # Inject from the project's .env.schema:
132
+ hq run -- <your command>
133
+ # Or explicitly by name:
134
+ hq secrets exec --only $SECRET_NAMES --company $COMPANY -- <your command>
135
+ \`\`\`
136
+ "
137
+ fi
138
+
139
+ # --- write the prompt (staged, scanned, then placed) -------------------------
140
+
141
+ STAGE="$(mktemp -d)"
142
+ trap 'rm -rf "$STAGE"; [ "$SEND" -eq 0 ] || rmdir "$LOCK"' EXIT
143
+
144
+ {
145
+ echo "# Project handoff: $PROJECT"
146
+ echo
147
+ if [ "$MODE" = "share" ]; then
148
+ echo "$FROM has shared the **$PROJECT** project ($COMPANY) with you${NOTE:+ — $NOTE}. You are working on it together; ownership stays with them."
149
+ else
150
+ echo "$FROM has delegated the **$PROJECT** project ($COMPANY) to you${NOTE:+ — $NOTE}. Transfer status is recorded in the dossier. Work Mesh ownership requires separate confirmation. Delegation id: \`$DELEGATION_ID\`."
151
+ fi
152
+ echo
153
+ echo "Completed grant and publication steps are recorded as receipts in the manifest. Sender-side checks do not prove your access: pull the files below and acknowledge receipt, or report any access error."
154
+ echo
155
+ if [ -n "$GOAL" ]; then
156
+ echo "## The goal"
157
+ echo
158
+ echo "$GOAL"
159
+ echo
160
+ fi
161
+ if [ -n "$STATE_LINE" ]; then
162
+ echo "## Where things stand"
163
+ echo
164
+ echo "$STATE_LINE"
165
+ echo
166
+ fi
167
+ echo "## Pull the files (on demand — no sync)"
168
+ echo
169
+ echo "Run these from your HQ root; each materializes exactly that path locally:"
170
+ echo
171
+ echo '```bash'
172
+ printf '%s\n' "$GET_LINES"
173
+ echo '```'
174
+ echo
175
+ echo "Start with the project dossier: \`companies/$COMPANY/projects/$PROJECT/\` now contains the PRD (\`prd.json\`), a full brief (\`README.md\` / the DM's details pane), and the delegation journal."
176
+ echo
177
+ if [ -n "$REPO_SECTION" ]; then
178
+ printf '%s\n' "$REPO_SECTION"
179
+ echo
180
+ fi
181
+ if [ -n "$SECRETS_SECTION" ]; then
182
+ printf '%s\n' "$SECRETS_SECTION"
183
+ echo
184
+ fi
185
+ if [ -n "$NEXT_STEPS" ]; then
186
+ echo "## Your next three steps"
187
+ echo
188
+ printf '%s\n' "$NEXT_STEPS"
189
+ echo
190
+ fi
191
+ echo "## If something is off"
192
+ echo
193
+ echo "The manifest and BRIEF.md are published under \`delegation/$DELEGATION_ID/\` within \`companies/$COMPANY/projects/$PROJECT/\`. If a pull fails or a path looks empty, report it to $FROM."
194
+ } > "$STAGE/PICKUP-PROMPT.md"
195
+
196
+ # fail-closed scans: secret shapes + share-session capability URLs
197
+ if ! hq_scan_secrets "$STAGE/PICKUP-PROMPT.md" "$BRIEF"; then
198
+ die "generated pickup prompt matched a secret-detection pattern — aborting, nothing written or sent"
199
+ fi
200
+ if grep -Eq 'share-session/[A-Za-z0-9_-]+' "$STAGE/PICKUP-PROMPT.md" "$BRIEF" 2>/dev/null; then
201
+ die "a share-session URL appeared in a delegation artifact — forbidden (direct grants only), aborting"
202
+ fi
203
+
204
+ mv "$STAGE/PICKUP-PROMPT.md" "$PROMPT"
205
+ rm -rf "$STAGE"
206
+ trap '[ "$SEND" -eq 0 ] || rmdir "$LOCK"' EXIT
207
+
208
+ echo "hq-delegate-send: pickup prompt written: $PROMPT"
209
+
210
+ [ "$SEND" -eq 1 ] || exit 0
211
+
212
+ # --- deliver: ONE hq dm invocation, file flags only --------------------------
213
+
214
+ STATUS="$(jq -r '.status' "$MANIFEST")"
215
+ [ "$STATUS" = "verified" ] \
216
+ || die "manifest status is '$STATUS' — the reachability probe (hq-delegate-verify.sh) must pass before sending"
217
+ [ -f "$BRIEF" ] || die "BRIEF.md missing from the bundle — cannot send without the details pane"
218
+ jq -e '.publication.verifiedAt != null and (.publication.files | type == "object" and length > 0)' "$MANIFEST" >/dev/null \
219
+ || die "verified publication receipt missing; run verification again"
220
+ file_hash() {
221
+ if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d ' ' -f1; else shasum -a 256 "$1" | cut -d ' ' -f1; fi
222
+ }
223
+ while IFS= read -r row; do
224
+ path="$(printf '%s' "$row" | jq -r '.key')"
225
+ expected="$(printf '%s' "$row" | jq -r '.value')"
226
+ [ -f "$HQ_ROOT/$path" ] && [ "$(file_hash "$HQ_ROOT/$path")" = "$expected" ] \
227
+ || die "published artifact changed: $path; run verification again"
228
+ done < <(jq -c '.publication.files | to_entries[]' "$MANIFEST")
229
+ PUBLISHED="$HQ_ROOT/$(jq -r '.publication.manifestPath' "$MANIFEST")"
230
+ [ -f "$PUBLISHED" ] || die "published manifest missing; run verification again"
231
+ NORMALIZED="$(jq -cS '.status="granted" | .recipientAccess="unconfirmed" | del(.publication,.verifiedAt,.sentAt,.dmEventId)' "$MANIFEST")"
232
+ [ "$NORMALIZED" = "$(jq -cS . "$PUBLISHED")" ] || die "manifest changed since publication; run verification again"
233
+ [ "$(file_hash "$BRIEF")" = "$(file_hash "$(dirname "$PUBLISHED")/BRIEF.md")" ] \
234
+ || die "brief changed since publication; run verification again"
235
+
236
+ [ -n "$HEADLINE" ] || HEADLINE="Project handoff: $PROJECT — open the brief, pull the files, and acknowledge receipt"
237
+
238
+ TMP_MANIFEST="$(mktemp "${MANIFEST}.tmp.XXXXXX")"
239
+ if ! jq '.status = "sending"' "$MANIFEST" > "$TMP_MANIFEST" || ! mv "$TMP_MANIFEST" "$MANIFEST"; then
240
+ rm -f "$TMP_MANIFEST"
241
+ die "could not persist sending state; no DM attempted"
242
+ fi
243
+ DM_OUT="$(hq dm "$PRINCIPAL" "$HEADLINE" --prompt-file "$PROMPT" --details-file "$BRIEF")" \
244
+ || die "hq dm delivery outcome is uncertain; status is sending, inspect DM history before retrying"
245
+
246
+ EVENT_ID="$(printf '%s' "$DM_OUT" | grep -oE 'eventId[: ]+[A-Za-z0-9_-]+' | head -1 | sed -E 's/eventId[: ]+//' || true)"
247
+ [ -n "$EVENT_ID" ] || die "DM response has no receipt; status is sending, reconcile DM history before retrying"
248
+ NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
249
+
250
+ TMP_MANIFEST="$(mktemp "${MANIFEST}.tmp.XXXXXX")"
251
+ if ! jq --arg now "$NOW" --arg ev "${EVENT_ID:-}" \
252
+ '.status = "sent" | .sentAt = $now
253
+ | .dmEventId = (if $ev == "" then null else $ev end)' \
254
+ "$MANIFEST" > "$TMP_MANIFEST" || ! mv "$TMP_MANIFEST" "$MANIFEST"; then
255
+ rm -f "$TMP_MANIFEST"
256
+ die "DM delivered but receipt could not be saved; reconcile delivery before retrying"
257
+ fi
258
+
259
+ echo "hq-delegate-send: DM delivered to $DISPLAY ($PRINCIPAL)${EVENT_ID:+ (eventId $EVENT_ID)} — status advanced to 'sent'"
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env bash
2
+ # hq-core: public
3
+ # hq-delegate-verify.sh — prove a delegation can actually be picked up
4
+ # before anyone is told it is theirs, or print the full plan without
5
+ # touching anything.
6
+ #
7
+ # Usage (probe mode — after grants, before the DM):
8
+ # core/scripts/hq-delegate-verify.sh --manifest <path>
9
+ #
10
+ # Probes every manifest vaultPrefix with `hq files browse` and requires it
11
+ # reachable AND non-empty. All pass -> manifest advances granted -> verified
12
+ # (send is gated on this). Any failure -> non-zero, the failing prefix and
13
+ # likely cause named, status left at its last successful value so a re-run
14
+ # resumes without re-granting.
15
+ #
16
+ # Usage (dry-run mode — full plan, zero mutation):
17
+ # core/scripts/hq-delegate-verify.sh --dry-run \
18
+ # --company <slug> --project <name> --to <principal> [--mode transfer|share]
19
+ #
20
+ # Prints recipient, mode, every prefix with its permission, the secret
21
+ # names an .env.schema would grant, repo + branch, and the DM headline that
22
+ # would be sent. Exits 0. Creates nothing under workspace/delegations/ and
23
+ # invokes no mutating command.
24
+ #
25
+ # Env: HQ_ROOT — HQ root override (default: resolved from script location)
26
+
27
+ set -euo pipefail
28
+
29
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
30
+ HQ_ROOT="${HQ_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
31
+
32
+ usage() { sed -n '3,25p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
33
+ die() { echo "hq-delegate-verify: $*" >&2; exit 1; }
34
+
35
+ command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
36
+
37
+ MANIFEST="" DRY_RUN=0 COMPANY="" PROJECT="" TO="" MODE="transfer"
38
+ while [ $# -gt 0 ]; do
39
+ case "$1" in
40
+ --manifest) MANIFEST="${2:-}"; shift 2 ;;
41
+ --dry-run) DRY_RUN=1; shift ;;
42
+ --company) COMPANY="${2:-}"; shift 2 ;;
43
+ --project) PROJECT="${2:-}"; shift 2 ;;
44
+ --to) TO="${2:-}"; shift 2 ;;
45
+ --mode) MODE="${2:-}"; shift 2 ;;
46
+ -h|--help) usage; exit 0 ;;
47
+ *) die "unknown argument: $1" ;;
48
+ esac
49
+ done
50
+
51
+ # =============================================================================
52
+ # Dry-run mode: full plan from the builder's own derivation, zero mutation
53
+ # =============================================================================
54
+
55
+ if [ "$DRY_RUN" -eq 1 ]; then
56
+ [ -n "$COMPANY" ] || die "--dry-run requires --company"
57
+ [ -n "$PROJECT" ] || die "--dry-run requires --project"
58
+ [ -n "$TO" ] || die "--dry-run requires --to"
59
+
60
+ # The builder's --dry-run prints the manifest it WOULD write — single
61
+ # derivation source, nothing lands on disk.
62
+ PLAN_JSON="$(HQ_ROOT="$HQ_ROOT" bash "$SCRIPT_DIR/hq-delegate-bundle.sh" build \
63
+ --company "$COMPANY" --project "$PROJECT" --to "$TO" --mode "$MODE" --dry-run)" \
64
+ || die "could not derive the delegation plan (see builder error above)"
65
+
66
+ # Secret names the US-005 step would grant (same .env.schema derivation).
67
+ REPO_REL="$(printf '%s' "$PLAN_JSON" | jq -r '.repo.path // empty')"
68
+ SCHEMA=""
69
+ for candidate in \
70
+ "$HQ_ROOT/companies/$COMPANY/projects/$PROJECT/.env.schema" \
71
+ "${REPO_REL:+$HQ_ROOT/$REPO_REL/.env.schema}"; do
72
+ [ -n "$candidate" ] && [ -f "$candidate" ] && { SCHEMA="$candidate"; break; }
73
+ done
74
+ SECRET_NAMES="(none — no .env.schema found)"
75
+ if [ -n "$SCHEMA" ]; then
76
+ SECRET_NAMES="$(grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$SCHEMA" | cut -d= -f1 | sort -u | tr '\n' ' ')"
77
+ [ -n "$SECRET_NAMES" ] || SECRET_NAMES="(none declared)"
78
+ fi
79
+
80
+ echo "Delegation dry run — nothing will be pushed, granted, transferred, or sent."
81
+ echo
82
+ echo " Recipient: $TO"
83
+ echo " Mode: $MODE"
84
+ echo " Company: $COMPANY"
85
+ echo " Project: $PROJECT"
86
+ echo
87
+ echo " Vault grants that would be written:"
88
+ printf '%s' "$PLAN_JSON" | jq -r '.vaultPrefixes[] | " \(.permission)\ton \(.prefix)\t(\(.reason // ""))"'
89
+ echo
90
+ echo " Secret names that would be granted (read): $SECRET_NAMES"
91
+ echo
92
+ if printf '%s' "$PLAN_JSON" | jq -e '.repo != null' >/dev/null; then
93
+ printf '%s' "$PLAN_JSON" | jq -r '" Repo handover: \(.repo.path) — branch \(.repo.branch // "(none)") (base \(.repo.baseBranch))"'
94
+ else
95
+ echo " Repo handover: none (project has no repoPath)"
96
+ fi
97
+ echo
98
+ echo " DM that would be sent to $TO:"
99
+ echo " \"Project handoff: $PROJECT is yours now — open details for the brief, paste the prompt into your agent to pick it up\""
100
+ echo " (prompt: generated pickup prompt; details: the delegation brief)"
101
+ exit 0
102
+ fi
103
+
104
+ # =============================================================================
105
+ # Probe mode: reachability of every granted prefix
106
+ # =============================================================================
107
+
108
+ [ -n "$MANIFEST" ] || die "--manifest is required (or --dry-run with --company/--project/--to)"
109
+ [ -f "$MANIFEST" ] || die "manifest not found: $MANIFEST"
110
+ jq -e . "$MANIFEST" >/dev/null 2>&1 || die "manifest is not valid JSON: $MANIFEST"
111
+
112
+ COMPANY="$(jq -r '.company // empty' "$MANIFEST")"
113
+ LOCK="$MANIFEST.send-lock"
114
+ mkdir "$LOCK" 2>/dev/null || die "verification or delivery is active; retry after it finishes"
115
+ REF_BROWSE_CACHE=""
116
+ trap '[[ -z "$REF_BROWSE_CACHE" ]] || rm -rf "$REF_BROWSE_CACHE"; rmdir "$LOCK"' EXIT
117
+ STATUS="$(jq -r '.status // empty' "$MANIFEST")"
118
+ [ -n "$COMPANY" ] || die "manifest has no company"
119
+
120
+ case "$STATUS" in
121
+ granted|verified) ;;
122
+ building) die "manifest status is 'building' — run hq-delegate-grant.sh first (probe verifies grants, it cannot create them)" ;;
123
+ *) die "manifest status is '$STATUS' — probe runs from 'granted' (or re-runs from 'verified')" ;;
124
+ esac
125
+
126
+ PREFIX_COUNT="$(jq '.vaultPrefixes | length' "$MANIFEST")"
127
+ [ "$PREFIX_COUNT" -gt 0 ] || die "manifest has no vaultPrefixes to probe"
128
+
129
+ echo "hq-delegate-verify: probing $PREFIX_COUNT prefix(es) in company '$COMPANY'"
130
+ FAILED=0
131
+ i=0
132
+ while [ "$i" -lt "$PREFIX_COUNT" ]; do
133
+ PFX="$(jq -r ".vaultPrefixes[$i].prefix" "$MANIFEST")"
134
+ BROWSE_OUT="$(hq files browse "$PFX" --company "$COMPANY" 2>&1)" && BROWSE_RC=0 || BROWSE_RC=$?
135
+ if [ "$BROWSE_RC" -ne 0 ]; then
136
+ echo " FAIL $PFX — browse errored (likely cause: the grant did not land, or the caller's session expired — re-run /hq-login and hq-delegate-grant.sh)"
137
+ FAILED=1
138
+ elif [ -z "$(printf '%s' "$BROWSE_OUT" | tr -d '[:space:]')" ]; then
139
+ echo " FAIL $PFX — reachable but EMPTY (likely cause: the dossier was never pushed to the vault — hq sync push companies/$COMPANY/... and re-run)"
140
+ FAILED=1
141
+ else
142
+ echo " pass $PFX"
143
+ fi
144
+ i=$((i + 1))
145
+ done
146
+
147
+ # --- referenced files: prefix reachability is not enough ---------------------
148
+ # (Live finding: a knowledge prefix can be reachable and non-empty while the
149
+ # SPECIFIC referenced file is absent — the recipient then pulls a folder that
150
+ # silently misses the note the brief points at. Probe each referenced file.)
151
+
152
+ # Memoized, throttle-tolerant browse for the referenced-file loop. Multiple
153
+ # referenced files can share a parent prefix (e.g. two policies under
154
+ # policies/), and back-to-back vault browses of the same prefix can be throttled
155
+ # to an EMPTY page — which the grep below would misread as "file not in vault".
156
+ # Browse each distinct parent at most once, and retry an empty result a few
157
+ # times before trusting it. (Cache dir cleaned on exit.)
158
+ REF_BROWSE_CACHE="$(mktemp -d)"
159
+ browse_parent() { # parent -> listing (memoized per run, retried on empty)
160
+ local parent="$1" key out attempt
161
+ key="$REF_BROWSE_CACHE/$(printf '%s' "$parent" | tr '/@:' '___')"
162
+ if [ -f "$key" ]; then cat "$key"; return 0; fi
163
+ attempt=1
164
+ out=""
165
+ while :; do
166
+ out="$(hq files browse "$parent" --company "$COMPANY" 2>/dev/null || true)"
167
+ [ -n "$(printf '%s' "$out" | tr -d '[:space:]')" ] && break
168
+ [ "$attempt" -ge "${HQ_DELEGATE_BROWSE_RETRIES:-3}" ] && break
169
+ attempt=$((attempt + 1))
170
+ sleep "${HQ_DELEGATE_BROWSE_RETRY_SLEEP:-2}"
171
+ done
172
+ printf '%s' "$out" > "$key"
173
+ printf '%s' "$out"
174
+ }
175
+
176
+ REF_COUNT="$(jq -r --arg co "$COMPANY" \
177
+ '[((.knowledge // []) + (.policies // []))[] | select(startswith("companies/" + $co + "/"))] | length' "$MANIFEST")"
178
+ if [ "$REF_COUNT" -gt 0 ]; then
179
+ echo "hq-delegate-verify: probing $REF_COUNT referenced knowledge/policy file(s)"
180
+ while IFS= read -r kpath; do
181
+ [ -n "$kpath" ] || continue
182
+ REL="${kpath#companies/$COMPANY/}"
183
+ PARENT="$(dirname "$REL")/"
184
+ BASE="$(basename "$REL")"
185
+ LISTING="$(browse_parent "$PARENT")"
186
+ # Pipe-free substring test: `printf | grep -q` lets grep close the pipe on an
187
+ # early match, which under `set -o pipefail` surfaces printf's SIGPIPE as a
188
+ # non-zero pipeline and FALSE-FAILS a file that is genuinely present (it bit
189
+ # exactly the referenced files that sort early in a large parent listing).
190
+ if [[ "$LISTING" == *"$BASE"* ]]; then
191
+ echo " pass $REL"
192
+ else
193
+ echo " FAIL $REL — granted prefix is reachable but this referenced file is NOT in the vault (likely cause: it was never pushed — hq sync push $kpath --company $COMPANY and re-run)"
194
+ FAILED=1
195
+ fi
196
+ done <<EOF
197
+ $(jq -r --arg co "$COMPANY" '((.knowledge // []) + (.policies // []))[] | select(startswith("companies/" + $co + "/"))' "$MANIFEST")
198
+ EOF
199
+ fi
200
+
201
+ if [ "$FAILED" -ne 0 ]; then
202
+ echo "hq-delegate-verify: probe FAILED — the DM must not be sent; fix the failing prefix(es) above and re-run (manifest status left at '$STATUS')" >&2
203
+ exit 1
204
+ fi
205
+
206
+ HQ_ROOT="$HQ_ROOT" bash "$SCRIPT_DIR/hq-delegate-publish.sh" --manifest "$MANIFEST" --lock-held \
207
+ || die "final dossier publication failed; notification blocked"
208
+ NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
209
+ TMP_MANIFEST="$(mktemp)"
210
+ jq --arg now "$NOW" '.status = "verified" | .verifiedAt = $now' \
211
+ "$MANIFEST" > "$TMP_MANIFEST" && mv "$TMP_MANIFEST" "$MANIFEST"
212
+
213
+ echo "hq-delegate-verify: all $PREFIX_COUNT prefix(es) reachable — manifest status advanced to 'verified'"