@indigoai-us/hq-cli 5.291.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 (40) hide show
  1. package/CHANGELOG.md +27 -8
  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/hq-delegate-bundle.sh +307 -0
  5. package/assets/scaffold/core/scripts/hq-delegate-grant.sh +226 -0
  6. package/assets/scaffold/core/scripts/hq-delegate-publish.sh +91 -0
  7. package/assets/scaffold/core/scripts/hq-delegate-send.sh +259 -0
  8. package/assets/scaffold/core/scripts/hq-delegate-verify.sh +213 -0
  9. package/assets/scaffold/core/scripts/hq-job-notify.sh +389 -0
  10. package/assets/scaffold/core/scripts/hq-job-probe.sh +835 -0
  11. package/assets/scaffold/core/scripts/hq-job-remediate.sh +359 -0
  12. package/assets/scaffold/core/scripts/hq-job-run.sh +1025 -0
  13. package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +830 -0
  14. package/dist/command-catalog.generated.d.ts +106 -1
  15. package/dist/command-catalog.generated.js +132 -1
  16. package/dist/commands/core-delegate-w1b.d.ts +8 -0
  17. package/dist/commands/core-delegate-w1b.js +1049 -0
  18. package/dist/commands/core-delegate.d.ts +28 -0
  19. package/dist/commands/core-delegate.js +15 -13
  20. package/dist/commands/core-handoff.js +152 -23
  21. package/dist/commands/core-jobs-native.d.ts +15 -0
  22. package/dist/commands/core-jobs-native.js +709 -2
  23. package/dist/commands/core-ontology.d.ts +22 -4
  24. package/dist/commands/core-ontology.js +90 -8
  25. package/dist/commands/core.js +10 -0
  26. package/dist/lib/checkpoint-ontology/drop-aliases.d.ts +41 -0
  27. package/dist/lib/checkpoint-ontology/drop-aliases.js +76 -0
  28. package/dist/lib/checkpoint-ontology/entity-file.d.ts +6 -3
  29. package/dist/lib/checkpoint-ontology/entity-file.js +12 -6
  30. package/dist/lib/checkpoint-ontology/merge-copies.d.ts +27 -8
  31. package/dist/lib/checkpoint-ontology/merge-copies.js +64 -97
  32. package/dist/lib/daemon/flag.d.ts +2 -0
  33. package/dist/lib/daemon/flag.js +2 -0
  34. package/dist/utils/scaffold-sibling-references.js +4 -4
  35. package/dist/utils/self-update.d.ts +6 -0
  36. package/dist/utils/self-update.js +28 -6
  37. package/dist/utils/staged-install.d.ts +34 -0
  38. package/dist/utils/staged-install.js +296 -0
  39. package/package.json +1 -1
  40. package/scripts/mesh-daemon-if-stale.mjs +4 -0
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env bash
2
+ # hq-core: public
3
+ # hq-delegate-grant.sh — materialize a delegation's dossier in the vault and
4
+ # write the ACL grants its manifest declares, verifying each one landed.
5
+ #
6
+ # Usage:
7
+ # core/scripts/hq-delegate-grant.sh --manifest <path> [--yes]
8
+ #
9
+ # Without --yes: prints the full grant plan (prefix, principal, permission —
10
+ # the write grant is a privilege escalation and needs explicit confirmation)
11
+ # and exits 2 without mutating anything. The caller (the /delegate skill)
12
+ # confirms with the user, then re-invokes with --yes.
13
+ #
14
+ # With --yes:
15
+ # 1. hq sync push the project directory so the vault prefix exists
16
+ # 2. hq files share each manifest vaultPrefix to the recipient
17
+ # 3. read each grant back with hq files acl and match grantee + permission
18
+ # 4. advance manifest status building -> granted, stamping verifiedAt
19
+ #
20
+ # Direct ACL grants ONLY — this helper never mints a share-session URL
21
+ # (policy hq-delegate-never-inlines-secrets-or-share-urls).
22
+ #
23
+ # Idempotent: a prefix whose grant already exists is reported, not an error.
24
+ # A failed read-back exits non-zero and leaves manifest status unchanged.
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,25p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
34
+ die() { echo "hq-delegate-grant: $*" >&2; exit 1; }
35
+
36
+ command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
37
+
38
+ MANIFEST="" YES=0
39
+ while [ $# -gt 0 ]; do
40
+ case "$1" in
41
+ --manifest) MANIFEST="${2:-}"; shift 2 ;;
42
+ --yes) YES=1; shift ;;
43
+ -h|--help) usage; exit 0 ;;
44
+ *) die "unknown argument: $1" ;;
45
+ esac
46
+ done
47
+
48
+ [ -n "$MANIFEST" ] || die "--manifest is required"
49
+ [ -f "$MANIFEST" ] || die "manifest not found: $MANIFEST"
50
+ jq -e . "$MANIFEST" >/dev/null 2>&1 || die "manifest is not valid JSON: $MANIFEST"
51
+
52
+ COMPANY="$(jq -r '.company // empty' "$MANIFEST")"
53
+ PROJECT="$(jq -r '.project.name // empty' "$MANIFEST")"
54
+ PRINCIPAL="$(jq -r '.to.principal // empty' "$MANIFEST")"
55
+ DISPLAY="$(jq -r '.to.displayName // .to.principal // empty' "$MANIFEST")"
56
+ STATUS="$(jq -r '.status // empty' "$MANIFEST")"
57
+ [ -n "$COMPANY" ] || die "manifest has no company"
58
+ [ -n "$PROJECT" ] || die "manifest has no project.name"
59
+ [ -n "$PRINCIPAL" ] || die "manifest has no to.principal"
60
+
61
+ # File-ACL principals must be an email, grp_<id>, or @all — an agentUid is
62
+ # NOT accepted by `hq files share`. For agent recipients, grants flow through
63
+ # a deterministic per-agent delegation group (the /new-agent pattern):
64
+ # grp_dlg-<uid tail>, containing exactly that agent.
65
+ GRANT_PRINCIPAL="$PRINCIPAL"
66
+ case "$PRINCIPAL" in
67
+ agt_*)
68
+ TAIL="$(printf '%s' "$PRINCIPAL" | tail -c 9 | tr '[:upper:]' '[:lower:]')"
69
+ GRANT_PRINCIPAL="grp_dlg-$TAIL"
70
+ ;;
71
+ esac
72
+
73
+ case "$STATUS" in
74
+ building|granted) ;;
75
+ *) die "manifest status is '$STATUS' — grants run from 'building' (or re-run from 'granted'), not from there" ;;
76
+ esac
77
+
78
+ PREFIX_COUNT="$(jq '.vaultPrefixes | length' "$MANIFEST")"
79
+ [ "$PREFIX_COUNT" -gt 0 ] || die "manifest has no vaultPrefixes"
80
+
81
+ # --- validate every prefix BEFORE touching anything --------------------------
82
+ # Company-relative, trailing-slash folder form. A bare prefix would degrade to
83
+ # a single literal key and grant nothing useful; company-anchored prefixes do
84
+ # not exist in the bucket. Either is a hard error, never a silent grant.
85
+
86
+ BAD="$(jq -r '.vaultPrefixes[] | .prefix
87
+ | select((endswith("/") | not) or startswith("companies/") or startswith("/"))' "$MANIFEST")"
88
+ if [ -n "$BAD" ]; then
89
+ die "prefix violates the hq-files prefix conventions (must be company-relative folder form ending in '/'): $BAD"
90
+ fi
91
+
92
+ # --- plan (always printed; the only output without --yes) --------------------
93
+
94
+ echo "Delegation grant plan — company '$COMPANY', recipient '$PRINCIPAL':"
95
+ if [ "$GRANT_PRINCIPAL" != "$PRINCIPAL" ]; then
96
+ echo " 0. Agent recipient: grants flow through group $GRANT_PRINCIPAL (created if absent, containing exactly $DISPLAY)"
97
+ fi
98
+ echo " 1. Push companies/$COMPANY/projects/$PROJECT/ to the vault (on-conflict keep)"
99
+ jq -r '.vaultPrefixes[] | " 2. Grant \(.permission) on \(.prefix) — \(.reason // "")"' "$MANIFEST"
100
+ WRITE_PREFIXES="$(jq -r '.vaultPrefixes[] | select(.permission == "write") | .prefix' "$MANIFEST")"
101
+ if [ -n "$WRITE_PREFIXES" ]; then
102
+ echo
103
+ echo "NOTE: granting 'write' is a privilege escalation. The recipient will be able"
104
+ echo "to upload, overwrite, and delete under: $(printf '%s ' "$WRITE_PREFIXES")"
105
+ fi
106
+
107
+ if [ "$YES" -ne 1 ]; then
108
+ echo
109
+ echo "hq-delegate-grant: confirmation required — re-run with --yes after the user approves" >&2
110
+ exit 2
111
+ fi
112
+
113
+ # --- 1. materialize the dossier AND referenced knowledge in the vault --------
114
+ # (Live finding: granting read on a knowledge prefix is useless if the
115
+ # specific referenced file was never pushed — the recipient's pull succeeds
116
+ # on the prefix and silently misses the file. Push every referenced
117
+ # company-local knowledge/policy path alongside the dossier.)
118
+
119
+ hq sync push "companies/$COMPANY/projects/$PROJECT/" --company "$COMPANY" --on-conflict keep \
120
+ || die "vault push failed for companies/$COMPANY/projects/$PROJECT/"
121
+
122
+ jq -r --arg co "$COMPANY" \
123
+ '((.knowledge // []) + (.policies // []))[] | select(startswith("companies/" + $co + "/"))' \
124
+ "$MANIFEST" | while IFS= read -r kpath; do
125
+ if [ -e "$HQ_ROOT/$kpath" ]; then
126
+ hq sync push "$kpath" --company "$COMPANY" --on-conflict keep \
127
+ || die "vault push failed for referenced knowledge: $kpath"
128
+ else
129
+ echo "hq-delegate-grant: referenced path missing locally (cannot push): $kpath — the verify probe will fail if it is not already in the vault" >&2
130
+ fi
131
+ done
132
+
133
+ # --- 1b. agent recipients: ensure the delegation group exists + contains them
134
+
135
+ if [ "$GRANT_PRINCIPAL" != "$PRINCIPAL" ]; then
136
+ # Both calls are idempotent-tolerated: an existing group / existing member
137
+ # is fine — the ACL read-back below is the arbiter of success.
138
+ hq groups create "$GRANT_PRINCIPAL" --name "Delegation: $DISPLAY" --company "$COMPANY" \
139
+ || echo "hq-delegate-grant: group create reported an error (may already exist) — continuing" >&2
140
+ hq groups add "$GRANT_PRINCIPAL" "$PRINCIPAL" --company "$COMPANY" \
141
+ || echo "hq-delegate-grant: group add reported an error (may already be a member) — continuing" >&2
142
+ fi
143
+
144
+ # --- 2+3. exact structured read-back; never infer identity from a new row ---
145
+ # Requires the structured CLI contracts. Unsupported/failed reads stop the flow.
146
+ EXPECTED_TYPE="" EXPECTED_ID="" EXPECTED_COMPANY=""
147
+ case "$GRANT_PRINCIPAL" in
148
+ grp_*) EXPECTED_TYPE=group; EXPECTED_ID="$GRANT_PRINCIPAL" ;;
149
+ @all) EXPECTED_TYPE=company-wide; EXPECTED_ID="" ;;
150
+ *)
151
+ RESOLVE_RC=0
152
+ RESOLVED="$(hq people resolve "$PRINCIPAL" --company "$COMPANY" --json --membership-only)" || RESOLVE_RC=$?
153
+ if printf '%s' "$RESOLVED" | jq -e '.status == "found" and (.person.personUid | type == "string") and (.person.companyUid | type == "string")' >/dev/null; then
154
+ [ "$RESOLVE_RC" -eq 0 ] || die "recipient resolution failed"
155
+ EXPECTED_TYPE=person
156
+ EXPECTED_ID="$(printf '%s' "$RESOLVED" | jq -r '.person.personUid')"
157
+ EXPECTED_COMPANY="$(printf '%s' "$RESOLVED" | jq -r '.person.companyUid')"
158
+ elif printf '%s' "$RESOLVED" | jq -e '.status == "not_found"' >/dev/null; then
159
+ case "$PRINCIPAL" in *@*) EXPECTED_TYPE=email; EXPECTED_ID="$(printf '%s' "$PRINCIPAL" | tr '[:upper:]' '[:lower:]' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" ;; *) die "unresolved recipient" ;; esac
160
+ else
161
+ die "authoritative recipient lookup failed; update hq-cli if --membership-only is unsupported"
162
+ fi
163
+ ;;
164
+ esac
165
+
166
+ read_acl() {
167
+ local result
168
+ result="$(hq files acl "$1" --company "$COMPANY" --json)" || return 1
169
+ printf '%s' "$result" | jq -e --arg prefix "$1" --arg company "$EXPECTED_COMPANY" '
170
+ .schemaVersion == 1 and
171
+ (
172
+ .prefix == $prefix or
173
+ (($prefix | endswith("/*")) and .prefix == ($prefix | sub("\\*$"; "")))
174
+ ) and
175
+ (.companyUid | type == "string") and ($company == "" or .companyUid == $company) and
176
+ (.direct | type == "array") and (.exists | type == "boolean")
177
+ ' >/dev/null || { echo "hq-delegate-grant: invalid structured ACL response for $1" >&2; return 1; }
178
+ printf '%s' "$result"
179
+ }
180
+
181
+ NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
182
+ i=0
183
+ while [ "$i" -lt "$PREFIX_COUNT" ]; do
184
+ PFX="$(jq -r ".vaultPrefixes[$i].prefix" "$MANIFEST")"
185
+ PERM="$(jq -r ".vaultPrefixes[$i].permission" "$MANIFEST")"
186
+ # Always address the recursive "folder/*" pattern explicitly. Since hq-pro
187
+ # #3662 (2026-09-23) the server echoes the raw pattern back on read, and a
188
+ # bare trailing "/" can mean a PRIVATE folder pattern when that flag is on.
189
+ # A delegation grant is always the recursive share, never a private folder.
190
+ ACL_PATTERN="${PFX}*"
191
+ BEFORE="$(read_acl "$ACL_PATTERN")" || die "ACL preflight failed; no share attempted for $PFX"
192
+ EXPECTED_COMPANY="$(printf '%s' "$BEFORE" | jq -r '.companyUid')"
193
+ SHARE_RC=0
194
+ hq files share "$ACL_PATTERN" --with "$GRANT_PRINCIPAL" --permission "$PERM" --company "$COMPANY" || SHARE_RC=$?
195
+ [ "$SHARE_RC" -eq 0 ] || echo "hq-delegate-grant: share reported an error on $PFX; checking exact grant" >&2
196
+ landed=0
197
+ for delay in 0 1 2; do
198
+ [ "$delay" -eq 0 ] || sleep "$delay"
199
+ if AFTER="$(read_acl "$ACL_PATTERN")" && printf '%s' "$AFTER" | jq -e \
200
+ --arg id "$EXPECTED_ID" --arg type "$EXPECTED_TYPE" --arg perm "$PERM" \
201
+ '.exists and any(.direct[]; .granteeType == $type and .granteeId == $id and .permission == $perm)' >/dev/null; then
202
+ landed=1
203
+ break
204
+ fi
205
+ done
206
+ [ "$landed" -eq 1 ] || die "grant outcome unconfirmed for $PRINCIPAL on $PFX; retain this manifest and resume"
207
+ TMP_MANIFEST="$(mktemp)"
208
+ jq --argjson i "$i" --arg now "$NOW" --arg id "$EXPECTED_ID" --arg type "$EXPECTED_TYPE" \
209
+ --arg company "$EXPECTED_COMPANY" --argjson rc "$SHARE_RC" '
210
+ .vaultPrefixes[$i].verifiedAt = $now |
211
+ .vaultPrefixes[$i].grantReceipt = {companyUid:$company,granteeType:$type,granteeId:$id,shareExitCode:$rc,verifiedAt:$now}
212
+ ' "$MANIFEST" > "$TMP_MANIFEST" && mv "$TMP_MANIFEST" "$MANIFEST"
213
+ echo "hq-delegate-grant: verified $PERM on $PFX for $PRINCIPAL"
214
+ i=$((i + 1))
215
+ done
216
+
217
+ # --- 4. advance manifest status ----------------------------------------------
218
+
219
+ TMP_MANIFEST="$(mktemp)"
220
+ jq --arg now "$NOW" --arg gp "$GRANT_PRINCIPAL" '
221
+ .status = "granted"
222
+ | .grantPrincipal = $gp
223
+ | .vaultPrefixes = [.vaultPrefixes[] | . + {verifiedAt: $now}]
224
+ ' "$MANIFEST" > "$TMP_MANIFEST" && mv "$TMP_MANIFEST" "$MANIFEST"
225
+
226
+ echo "hq-delegate-grant: all $PREFIX_COUNT grants verified — manifest status advanced to 'granted'"
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env bash
2
+ # hq-core: public
3
+ # Publish final delegation artifacts and verify their canonical cloud bytes.
4
+ set -euo pipefail
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ HQ_ROOT="${HQ_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
7
+ . "$SCRIPT_DIR/lib/secret-patterns.sh"
8
+ die() { echo "hq-delegate-publish: $*" >&2; exit 1; }
9
+ [ "${1:-}" = --manifest ] && [ -f "${2:-}" ] || die 'usage: --manifest <path>'
10
+ MANIFEST="$2"
11
+ LOCK="$MANIFEST.send-lock"
12
+ if [ "${3:-}" = --lock-held ]; then
13
+ [ -d "$LOCK" ] || die 'verification lock missing'
14
+ OWN_LOCK=0
15
+ else
16
+ [ "$#" -eq 2 ] || die 'usage: --manifest <path>'
17
+ mkdir "$LOCK" 2>/dev/null || die 'verification or delivery is active; retry after it finishes'
18
+ OWN_LOCK=1
19
+ fi
20
+ STAGE=""
21
+ trap '[[ -z "$STAGE" ]] || rm -rf "$STAGE"; [ "$OWN_LOCK" -eq 0 ] || rmdir "$LOCK"' EXIT
22
+ COMPANY="$(jq -er '.company' "$MANIFEST")"
23
+ PROJECT="$(jq -er '.project.name' "$MANIFEST")"
24
+ ID="$(jq -er '.delegationId' "$MANIFEST")"
25
+ for part in "$COMPANY" "$PROJECT" "$ID"; do
26
+ [[ "$part" =~ ^[A-Za-z0-9_-][A-Za-z0-9._-]*$ ]] || die 'unsafe path segment'
27
+ done
28
+ jq -e '.status == "granted" or .status == "verified"' "$MANIFEST" >/dev/null || die 'confirmed grants required'
29
+ if [ "$(jq -r '.mode' "$MANIFEST")" = transfer ]; then
30
+ jq -e '.ownershipTransferredAt != null' "$MANIFEST" >/dev/null || die 'transfer must finish before publication'
31
+ fi
32
+ REL="companies/$COMPANY/projects/$PROJECT"
33
+ PROJECT_DIR="$HQ_ROOT/$REL"
34
+ BUNDLE="$(cd "$(dirname "$MANIFEST")" && pwd)"
35
+ [ -f "$BUNDLE/BRIEF.md" ] || die 'BRIEF.md missing'
36
+ [ -f "$PROJECT_DIR/prd.json" ] || die 'PRD missing'
37
+ if [ "$(jq -r '.mode' "$MANIFEST")" = transfer ]; then
38
+ EXPECTED_OWNER="$(jq -r '.to.principal' "$MANIFEST")"
39
+ jq -e --arg owner "$EXPECTED_OWNER" '.metadata.owner == $owner' "$PROJECT_DIR/prd.json" >/dev/null || die 'local PRD owner does not match recipient'
40
+ fi
41
+ STAGE="$(mktemp -d)"
42
+ atomic_copy() {
43
+ local target_tmp
44
+ target_tmp="$(mktemp "${2}.tmp.XXXXXX")"
45
+ if ! cp "$1" "$target_tmp"; then rm -f "$target_tmp"; return 1; fi
46
+ mv -f "$target_tmp" "$2"
47
+ }
48
+ DEST="$PROJECT_DIR/delegation/$ID"
49
+ cp "$BUNDLE/BRIEF.md" "$STAGE/BRIEF.md"
50
+ file_hash() {
51
+ if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d ' ' -f1; else shasum -a 256 "$1" | cut -d ' ' -f1; fi
52
+ }
53
+ # Snapshot final project content, excluding mutable/self-referential receipts.
54
+ : > "$STAGE/hashes"
55
+ for file in "$PROJECT_DIR/prd.json" "$PROJECT_DIR/README.md" "$PROJECT_DIR/journal/delegations.md"; do
56
+ [ -f "$file" ] || continue
57
+ jq -cn --arg key "${file#"$HQ_ROOT/"}" --arg value "$(file_hash "$file")" '{key:$key,value:$value}' >> "$STAGE/hashes"
58
+ done
59
+ jq -cn --arg key "$REL/delegation/$ID/BRIEF.md" --arg value "$(file_hash "$STAGE/BRIEF.md")" '{key:$key,value:$value}' >> "$STAGE/hashes"
60
+ jq -s 'from_entries' "$STAGE/hashes" > "$STAGE/checksums"
61
+ jq --slurpfile hashes "$STAGE/checksums" '.checksums=$hashes[0] | .status="granted" | .recipientAccess="unconfirmed" | del(.publication,.verifiedAt,.sentAt,.dmEventId)' "$MANIFEST" > "$STAGE/manifest.json"
62
+ hq_scan_secrets "$STAGE/manifest.json" "$STAGE/BRIEF.md" || die 'unsafe generated dossier'
63
+ if grep -Eq 'share-session/[A-Za-z0-9_-]+' "$STAGE/manifest.json" "$STAGE/BRIEF.md"; then
64
+ die 'share-session capability in dossier; nothing published'
65
+ fi
66
+ mkdir -p "$DEST"
67
+ atomic_copy "$STAGE/BRIEF.md" "$DEST/BRIEF.md"
68
+ atomic_copy "$STAGE/manifest.json" "$DEST/manifest.json"
69
+ atomic_copy "$STAGE/manifest.json" "$MANIFEST"
70
+ # keep may preserve stale canonical content. Read-back must catch that instead
71
+ # of overwriting concurrent changes merely to make the check pass.
72
+ hq sync push "$REL/" --company "$COMPANY" --on-conflict keep || die 'final dossier push failed; resume publication'
73
+ cp "$STAGE/hashes" "$STAGE/receipts"
74
+ jq -cn --arg key "$REL/delegation/$ID/manifest.json" --arg value "$(file_hash "$DEST/manifest.json")" '{key:$key,value:$value}' >> "$STAGE/receipts"
75
+ while IFS= read -r row; do
76
+ path="$(printf '%s' "$row" | jq -r '.key')"
77
+ expected="$(printf '%s' "$row" | jq -r '.value')"
78
+ matched=0
79
+ for delay in 0 1 2; do
80
+ [ "$delay" -eq 0 ] || sleep "$delay"
81
+ if hq files cat "${path#"companies/$COMPANY/"}" --company "$COMPANY" > "$STAGE/remote"; then
82
+ if [ "$(file_hash "$STAGE/remote")" = "$expected" ]; then matched=1; break; fi
83
+ fi
84
+ done
85
+ [ "$matched" -eq 1 ] || die "canonical cloud content differs or is unreadable: $path; resolve before sending"
86
+ done < "$STAGE/receipts"
87
+ jq -s 'from_entries' "$STAGE/receipts" > "$STAGE/files"
88
+ jq --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg path "$REL/delegation/$ID/manifest.json" --slurpfile files "$STAGE/files" \
89
+ '.publication={verifiedAt:$now,manifestPath:$path,files:$files[0]} | .recipientAccess="unconfirmed"' "$MANIFEST" > "$STAGE/updated"
90
+ atomic_copy "$STAGE/updated" "$MANIFEST"
91
+ echo 'hq-delegate-publish: canonical dossier bytes verified as sender; recipient access unconfirmed'
@@ -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'"