@indigoai-us/hq-cli 5.291.0 → 5.293.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.
- package/CHANGELOG.md +53 -8
- package/assets/scaffold/core/scripts/handoff-finalize.sh +18 -0
- package/assets/scaffold/core/scripts/handoff-post.sh +71 -6
- package/assets/scaffold/core/scripts/hq-delegate-bundle.sh +307 -0
- package/assets/scaffold/core/scripts/hq-delegate-grant.sh +226 -0
- package/assets/scaffold/core/scripts/hq-delegate-publish.sh +91 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +259 -0
- package/assets/scaffold/core/scripts/hq-delegate-verify.sh +213 -0
- package/assets/scaffold/core/scripts/hq-job-notify.sh +389 -0
- package/assets/scaffold/core/scripts/hq-job-probe.sh +835 -0
- package/assets/scaffold/core/scripts/hq-job-remediate.sh +359 -0
- package/assets/scaffold/core/scripts/hq-job-run.sh +1025 -0
- package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +830 -0
- package/dist/command-catalog.generated.d.ts +109 -1
- package/dist/command-catalog.generated.js +136 -1
- package/dist/commands/core-delegate-w1b.d.ts +8 -0
- package/dist/commands/core-delegate-w1b.js +1049 -0
- package/dist/commands/core-delegate.d.ts +28 -0
- package/dist/commands/core-delegate.js +15 -13
- package/dist/commands/core-handoff.js +152 -23
- package/dist/commands/core-jobs-native.d.ts +15 -0
- package/dist/commands/core-jobs-native.js +709 -2
- package/dist/commands/core-ontology.d.ts +22 -4
- package/dist/commands/core-ontology.js +90 -8
- package/dist/commands/core.js +10 -0
- package/dist/commands/lanes-move.js +54 -1
- package/dist/commands/lanes-questions.js +4 -0
- package/dist/commands/lanes.d.ts +2 -0
- package/dist/commands/lanes.js +119 -5
- package/dist/lib/checkpoint-ontology/drop-aliases.d.ts +41 -0
- package/dist/lib/checkpoint-ontology/drop-aliases.js +76 -0
- package/dist/lib/checkpoint-ontology/entity-file.d.ts +6 -3
- package/dist/lib/checkpoint-ontology/entity-file.js +12 -6
- package/dist/lib/checkpoint-ontology/merge-copies.d.ts +27 -8
- package/dist/lib/checkpoint-ontology/merge-copies.js +64 -97
- package/dist/lib/core-utils/sentry-report.js +2 -2
- package/dist/lib/daemon/flag.d.ts +2 -0
- package/dist/lib/daemon/flag.js +2 -0
- package/dist/lib/daemon/run.d.ts +11 -0
- package/dist/lib/daemon/run.js +17 -1
- package/dist/lib/lanes/autopilot.d.ts +13 -1
- package/dist/lib/lanes/autopilot.js +271 -28
- package/dist/lib/lanes/grants.d.ts +1 -1
- package/dist/lib/lanes/grants.js +7 -5
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +5 -0
- package/dist/lib/lanes/keep-alive.js +9 -4
- package/dist/lib/lanes/questions.d.ts +2 -1
- package/dist/lib/lanes/questions.js +2 -0
- package/dist/lib/lanes/reconcile.js +186 -9
- package/dist/lib/lanes/spawn.js +4 -0
- package/dist/lib/lanes/store.js +21 -1
- package/dist/lib/lanes/types.d.ts +13 -1
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.js +3 -1
- package/dist/utils/scaffold-sibling-references.js +4 -4
- package/dist/utils/self-update.d.ts +13 -0
- package/dist/utils/self-update.js +33 -7
- package/dist/utils/staged-install.d.ts +34 -0
- package/dist/utils/staged-install.js +296 -0
- package/dist/utils/version-gate.d.ts +15 -0
- package/dist/utils/version-gate.js +46 -2
- package/package.json +1 -1
- 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'"
|