@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.
- package/CHANGELOG.md +66 -3
- 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/hook-lib.sh +345 -50
- 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/hq-session.sh +182 -21
- package/assets/scaffold/core/scripts/jobs-schedule-parse.sh +310 -0
- package/assets/scaffold/core/scripts/jobs-validate.sh +451 -0
- package/assets/scaffold/core/scripts/lib/session-id.sh +3 -2
- package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +2 -1
- package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +830 -0
- package/dist/command-catalog.generated.d.ts +165 -1
- package/dist/command-catalog.generated.js +205 -1
- package/dist/commands/cloud.d.ts +26 -14
- package/dist/commands/cloud.js +207 -56
- 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 +23 -0
- package/dist/commands/core-jobs-native.js +1331 -0
- package/dist/commands/core-native-jobs-flag.d.ts +4 -0
- package/dist/commands/core-native-jobs-flag.js +7 -0
- package/dist/commands/core-ontology.d.ts +22 -4
- package/dist/commands/core-ontology.js +90 -8
- package/dist/commands/core-policy.js +24 -5
- package/dist/commands/core.js +17 -0
- package/dist/commands/integrations-core.js +7 -0
- package/dist/commands/lanes.js +111 -35
- package/dist/commands/members.js +5 -0
- package/dist/commands/mesh.js +46 -25
- package/dist/commands/personal-targeted-push-flag.d.ts +4 -0
- package/dist/commands/personal-targeted-push-flag.js +7 -0
- package/dist/commands/personal-targeted-push-paths.d.ts +3 -0
- package/dist/commands/personal-targeted-push-paths.js +67 -0
- package/dist/commands/secrets.js +9 -0
- package/dist/commands/skill.d.ts +3 -0
- package/dist/commands/skill.js +28 -0
- package/dist/fast-core.js +1 -0
- package/dist/lib/billing/free-month.d.ts +18 -0
- package/dist/lib/billing/free-month.js +31 -0
- package/dist/lib/billing/plan-limit-refusals.d.ts +69 -0
- package/dist/lib/billing/plan-limit-refusals.js +187 -0
- package/dist/lib/billing/plan-lock.d.ts +2 -6
- package/dist/lib/billing/plan-lock.js +5 -16
- 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/secret-patterns.d.ts +3 -1
- package/dist/lib/core-utils/secret-patterns.js +3 -2
- package/dist/lib/daemon/flag.d.ts +2 -0
- package/dist/lib/daemon/flag.js +2 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/autopilot.d.ts +69 -0
- package/dist/lib/lanes/autopilot.js +418 -0
- package/dist/lib/lanes/mesh-events.js +2 -0
- package/dist/lib/lanes/store.js +11 -6
- package/dist/lib/mesh/api.js +4 -0
- package/dist/lib/mesh/live/daemon/state.d.ts +7 -1
- package/dist/lib/mesh/live/daemon/state.js +34 -2
- package/dist/main.js +23 -12
- package/dist/utils/plan-gate-error.d.ts +24 -0
- package/dist/utils/plan-gate-error.js +219 -9
- package/dist/utils/scaffold-sibling-references.js +4 -4
- package/dist/utils/self-update.d.ts +6 -0
- package/dist/utils/self-update.js +28 -6
- package/dist/utils/staged-install.d.ts +34 -0
- package/dist/utils/staged-install.js +296 -0
- package/package.json +2 -2
- 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'"
|