@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,307 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# hq-core: public
|
|
3
|
+
# hq-delegate-bundle.sh — freeze a project into a portable delegation bundle.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# core/scripts/hq-delegate-bundle.sh build \
|
|
7
|
+
# --company <slug> --project <name> --to <principal> \
|
|
8
|
+
# [--to-kind person|agent] [--to-name <display name>] \
|
|
9
|
+
# [--mode transfer|share] [--dry-run]
|
|
10
|
+
#
|
|
11
|
+
# --dry-run prints the manifest JSON to stdout and writes nothing.
|
|
12
|
+
#
|
|
13
|
+
# Writes workspace/delegations/<delegationId>/{manifest.json,BRIEF.md} and
|
|
14
|
+
# prints the delegationId on stdout. The manifest is the single source of
|
|
15
|
+
# truth for every downstream delegation step (grants, verification, pickup
|
|
16
|
+
# prompt) — schema documented in
|
|
17
|
+
# core/knowledge/public/hq-core/delegation-bundle-spec.md.
|
|
18
|
+
#
|
|
19
|
+
# Fails closed (non-zero, nothing written) when:
|
|
20
|
+
# - required arguments are missing
|
|
21
|
+
# - the project directory or its prd.json does not exist / parses invalid
|
|
22
|
+
# - the generated manifest or BRIEF matches a secret-detection pattern
|
|
23
|
+
#
|
|
24
|
+
# Env:
|
|
25
|
+
# HQ_ROOT — HQ root override (default: resolved from this script's 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
|
+
# shellcheck source=lib/secret-patterns.sh
|
|
33
|
+
. "$SCRIPT_DIR/lib/secret-patterns.sh"
|
|
34
|
+
|
|
35
|
+
usage() {
|
|
36
|
+
sed -n '3,13p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
die() {
|
|
40
|
+
echo "hq-delegate-bundle: $*" >&2
|
|
41
|
+
exit 1
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
|
|
45
|
+
|
|
46
|
+
# --- argument parsing --------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
[ "${1:-}" = "build" ] || { usage >&2; exit 1; }
|
|
49
|
+
shift
|
|
50
|
+
|
|
51
|
+
COMPANY="" PROJECT="" TO="" TO_KIND="person" TO_NAME="" MODE="transfer" DRY_RUN=0
|
|
52
|
+
while [ $# -gt 0 ]; do
|
|
53
|
+
case "$1" in
|
|
54
|
+
--company) COMPANY="${2:-}"; shift 2 ;;
|
|
55
|
+
--project) PROJECT="${2:-}"; shift 2 ;;
|
|
56
|
+
--to) TO="${2:-}"; shift 2 ;;
|
|
57
|
+
--to-kind) TO_KIND="${2:-}"; shift 2 ;;
|
|
58
|
+
--to-name) TO_NAME="${2:-}"; shift 2 ;;
|
|
59
|
+
--mode) MODE="${2:-}"; shift 2 ;;
|
|
60
|
+
--dry-run) DRY_RUN=1; shift ;;
|
|
61
|
+
-h|--help) usage; exit 0 ;;
|
|
62
|
+
*) die "unknown argument: $1" ;;
|
|
63
|
+
esac
|
|
64
|
+
done
|
|
65
|
+
|
|
66
|
+
[ -n "$COMPANY" ] || die "--company is required"
|
|
67
|
+
[ -n "$PROJECT" ] || die "--project is required"
|
|
68
|
+
[ -n "$TO" ] || die "--to is required"
|
|
69
|
+
case "$MODE" in transfer|share) ;; *) die "--mode must be transfer or share" ;; esac
|
|
70
|
+
case "$TO_KIND" in person|agent) ;; *) die "--to-kind must be person or agent" ;; esac
|
|
71
|
+
|
|
72
|
+
PROJECT_DIR="$HQ_ROOT/companies/$COMPANY/projects/$PROJECT"
|
|
73
|
+
PRD="$PROJECT_DIR/prd.json"
|
|
74
|
+
[ -d "$PROJECT_DIR" ] || die "project directory not found: companies/$COMPANY/projects/$PROJECT"
|
|
75
|
+
[ -f "$PRD" ] || die "project has no prd.json: companies/$COMPANY/projects/$PROJECT/prd.json"
|
|
76
|
+
jq -e . "$PRD" >/dev/null 2>&1 || die "prd.json is not valid JSON: $PRD"
|
|
77
|
+
|
|
78
|
+
# --- identity ----------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
CREATED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
81
|
+
DELEGATION_ID="dlg-$(date -u +%Y%m%d-%H%M%S)-$PROJECT"
|
|
82
|
+
WHOAMI_JSON="$(hq whoami --json)" || die "could not read sender identity; update hq-cli if --json is unsupported"
|
|
83
|
+
printf '%s' "$WHOAMI_JSON" | jq -e '.authenticated != false and ((.email // .personUid // .agentUid // "") | length > 0)' >/dev/null \
|
|
84
|
+
|| die "sender identity is missing or expired; run hq login"
|
|
85
|
+
FROM_EMAIL="$(printf '%s' "$WHOAMI_JSON" | jq -r '.email // empty')"
|
|
86
|
+
FROM_UID="$(printf '%s' "$WHOAMI_JSON" | jq -r '.personUid // .agentUid // empty')"
|
|
87
|
+
|
|
88
|
+
# --- checksum helper (macOS + Linux) -----------------------------------------
|
|
89
|
+
|
|
90
|
+
file_sha256() {
|
|
91
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
92
|
+
sha256sum "$1" | awk '{print $1}'
|
|
93
|
+
else
|
|
94
|
+
shasum -a 256 "$1" | awk '{print $1}'
|
|
95
|
+
fi
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# --- derive vault prefixes, knowledge, policies ------------------------------
|
|
99
|
+
#
|
|
100
|
+
# Vault prefixes are BUCKET-RELATIVE (never prefixed with companies/<slug>/)
|
|
101
|
+
# and always in trailing-slash folder form — see hq-files "Prefix Conventions".
|
|
102
|
+
|
|
103
|
+
PROJECT_PREFIX="projects/$PROJECT/"
|
|
104
|
+
|
|
105
|
+
# knowledge entries from prd metadata: split into vault-grantable (under this
|
|
106
|
+
# company), policies (under this company's policies/), and repo-based docs
|
|
107
|
+
# (recorded for the brief, never granted as vault prefixes).
|
|
108
|
+
KNOWLEDGE_JSON="$(jq -c '[.metadata.knowledge // [] | .[]]' "$PRD")"
|
|
109
|
+
|
|
110
|
+
VAULT_READ_PREFIXES="$(printf '%s' "$KNOWLEDGE_JSON" | jq -r --arg co "$COMPANY" '
|
|
111
|
+
[ .[]
|
|
112
|
+
| select(startswith("companies/" + $co + "/"))
|
|
113
|
+
| sub("^companies/" + $co + "/"; "")
|
|
114
|
+
# a file path -> its containing folder; a dir path -> itself with slash
|
|
115
|
+
| if endswith("/") then . else (split("/")[:-1] | join("/") + "/") end
|
|
116
|
+
] | unique | .[]')"
|
|
117
|
+
|
|
118
|
+
MANIFEST_KNOWLEDGE="$(printf '%s' "$KNOWLEDGE_JSON" | jq -c --arg co "$COMPANY" '
|
|
119
|
+
[ .[] | select((startswith("companies/" + $co + "/policies/")) | not) ] | unique')"
|
|
120
|
+
MANIFEST_POLICIES="$(printf '%s' "$KNOWLEDGE_JSON" | jq -c --arg co "$COMPANY" '
|
|
121
|
+
[ .[] | select(startswith("companies/" + $co + "/policies/")) ] | unique')"
|
|
122
|
+
|
|
123
|
+
# vaultPrefixes[]: write on the project dossier, read on referenced knowledge.
|
|
124
|
+
VAULT_PREFIXES_JSON="$(
|
|
125
|
+
{
|
|
126
|
+
jq -cn --arg p "$PROJECT_PREFIX" \
|
|
127
|
+
'{prefix: $p, permission: "write", reason: "project dossier"}'
|
|
128
|
+
if [ -n "$VAULT_READ_PREFIXES" ]; then
|
|
129
|
+
printf '%s\n' "$VAULT_READ_PREFIXES" | while IFS= read -r pfx; do
|
|
130
|
+
[ -n "$pfx" ] || continue
|
|
131
|
+
jq -cn --arg p "$pfx" \
|
|
132
|
+
'{prefix: $p, permission: "read", reason: "referenced knowledge/policies"}'
|
|
133
|
+
done
|
|
134
|
+
fi
|
|
135
|
+
} | jq -cs 'unique_by(.prefix)'
|
|
136
|
+
)"
|
|
137
|
+
|
|
138
|
+
# Guard: every prefix must be folder form and company-relative.
|
|
139
|
+
printf '%s' "$VAULT_PREFIXES_JSON" | jq -e '
|
|
140
|
+
all(.[]; (.prefix | endswith("/")) and (.prefix | startswith("companies/") | not))
|
|
141
|
+
' >/dev/null || die "internal error: derived a non-folder or company-anchored prefix"
|
|
142
|
+
|
|
143
|
+
# --- repo block (recorded here; verified/pushed by the US-004 step) ----------
|
|
144
|
+
|
|
145
|
+
REPO_PATH="$(jq -r '.metadata.repoPath // empty' "$PRD")"
|
|
146
|
+
if [ -n "$REPO_PATH" ]; then
|
|
147
|
+
REPO_JSON="$(jq -cn \
|
|
148
|
+
--arg path "$REPO_PATH" \
|
|
149
|
+
--arg branch "$(jq -r '.branchName // empty' "$PRD")" \
|
|
150
|
+
--arg base "$(jq -r '.metadata.baseBranch // "main"' "$PRD")" \
|
|
151
|
+
'{path: $path, remote: null, branch: (if $branch == "" then null else $branch end),
|
|
152
|
+
baseBranch: $base, headSha: null, dirtyFiles: [], accessVerified: false}')"
|
|
153
|
+
else
|
|
154
|
+
REPO_JSON="null"
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# --- board id ----------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
BOARD_ID="null"
|
|
160
|
+
BOARD_FILE="$HQ_ROOT/companies/$COMPANY/board.json"
|
|
161
|
+
if [ -f "$BOARD_FILE" ]; then
|
|
162
|
+
BOARD_ID="$(jq --arg prd "companies/$COMPANY/projects/$PROJECT/prd.json" \
|
|
163
|
+
'[.projects // [] | .[] | select(.prd_path == $prd) | .id] | first // null' \
|
|
164
|
+
"$BOARD_FILE" 2>/dev/null || echo null)"
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# --- checksums of the project dossier ----------------------------------------
|
|
168
|
+
|
|
169
|
+
CHECKSUMS_JSON="$(
|
|
170
|
+
cd "$HQ_ROOT" || exit 1
|
|
171
|
+
find "companies/$COMPANY/projects/$PROJECT" -type f ! -name '.DS_Store' | sort | head -200 | \
|
|
172
|
+
while IFS= read -r f; do
|
|
173
|
+
jq -cn --arg p "$f" --arg h "$(file_sha256 "$f")" '{key: $p, value: $h}'
|
|
174
|
+
done | jq -s 'from_entries'
|
|
175
|
+
)"
|
|
176
|
+
|
|
177
|
+
# --- build in a temp dir, scan, then move into place -------------------------
|
|
178
|
+
|
|
179
|
+
STAGE="$(mktemp -d)"
|
|
180
|
+
trap 'rm -rf "$STAGE"' EXIT
|
|
181
|
+
|
|
182
|
+
jq -n \
|
|
183
|
+
--arg id "$DELEGATION_ID" \
|
|
184
|
+
--arg createdAt "$CREATED_AT" \
|
|
185
|
+
--arg mode "$MODE" \
|
|
186
|
+
--arg fromEmail "$FROM_EMAIL" \
|
|
187
|
+
--arg fromUid "$FROM_UID" \
|
|
188
|
+
--arg toKind "$TO_KIND" \
|
|
189
|
+
--arg toPrincipal "$TO" \
|
|
190
|
+
--arg toName "$TO_NAME" \
|
|
191
|
+
--arg company "$COMPANY" \
|
|
192
|
+
--arg project "$PROJECT" \
|
|
193
|
+
--arg prdPath "companies/$COMPANY/projects/$PROJECT/prd.json" \
|
|
194
|
+
--argjson boardId "$BOARD_ID" \
|
|
195
|
+
--argjson vaultPrefixes "$VAULT_PREFIXES_JSON" \
|
|
196
|
+
--argjson repo "$REPO_JSON" \
|
|
197
|
+
--argjson knowledge "$MANIFEST_KNOWLEDGE" \
|
|
198
|
+
--argjson policies "$MANIFEST_POLICIES" \
|
|
199
|
+
--argjson checksums "$CHECKSUMS_JSON" \
|
|
200
|
+
'{
|
|
201
|
+
schemaVersion: 1,
|
|
202
|
+
delegationId: $id,
|
|
203
|
+
createdAt: $createdAt,
|
|
204
|
+
mode: $mode,
|
|
205
|
+
from: {email: (if $fromEmail == "" then null else $fromEmail end),
|
|
206
|
+
personUid: (if $fromUid == "" then null else $fromUid end)},
|
|
207
|
+
to: {kind: $toKind, principal: $toPrincipal,
|
|
208
|
+
displayName: (if $toName == "" then null else $toName end)},
|
|
209
|
+
company: $company,
|
|
210
|
+
project: {name: $project, prdPath: $prdPath, boardId: $boardId},
|
|
211
|
+
vaultPrefixes: $vaultPrefixes,
|
|
212
|
+
repo: $repo,
|
|
213
|
+
secrets: [],
|
|
214
|
+
knowledge: $knowledge,
|
|
215
|
+
policies: $policies,
|
|
216
|
+
checksums: $checksums,
|
|
217
|
+
status: "building"
|
|
218
|
+
}' > "$STAGE/manifest.json"
|
|
219
|
+
|
|
220
|
+
# --- BRIEF.md — full prose for a reader who has never seen the project -------
|
|
221
|
+
|
|
222
|
+
DESCRIPTION="$(jq -r '.description // "No description recorded."' "$PRD")"
|
|
223
|
+
GOAL="$(jq -r '.metadata.goal // empty' "$PRD")"
|
|
224
|
+
TOTAL_STORIES="$(jq -r '[.userStories // [] | .[]] | length' "$PRD")"
|
|
225
|
+
DONE_STORIES="$(jq -r '[.userStories // [] | .[] | select(.passes == true)] | length' "$PRD")"
|
|
226
|
+
DONE_LIST="$(jq -r '[.userStories // [] | .[] | select(.passes == true)] | .[] | "- \(.id): \(.title)"' "$PRD")"
|
|
227
|
+
NEXT_STEPS="$(jq -r '[.userStories // [] | .[] | select(.passes != true)]
|
|
228
|
+
| sort_by(.priority) | .[:3] | .[]
|
|
229
|
+
| "### \(.id): \(.title)\n\n\(.description)\n"' "$PRD")"
|
|
230
|
+
OPEN_QUESTIONS="$(jq -r '[.metadata.openQuestions // [] | .[]] | .[] | "- \(.)"' "$PRD")"
|
|
231
|
+
TRAPS="$(jq -r '[.metadata.securityNotes // empty, (.metadata.executionConventions // [] | .[])]
|
|
232
|
+
| .[] | "- \(.)"' "$PRD")"
|
|
233
|
+
|
|
234
|
+
{
|
|
235
|
+
echo "# Delegation brief — $PROJECT"
|
|
236
|
+
echo
|
|
237
|
+
echo "This brief describes the **$PROJECT** project in the **$COMPANY** company. Completed handoff steps are recorded in the manifest. Pull the files and acknowledge receipt; report any access error to the sender."
|
|
238
|
+
echo
|
|
239
|
+
echo "## What this project is"
|
|
240
|
+
echo
|
|
241
|
+
echo "$DESCRIPTION"
|
|
242
|
+
if [ -n "$GOAL" ]; then
|
|
243
|
+
echo
|
|
244
|
+
echo "## The goal"
|
|
245
|
+
echo
|
|
246
|
+
echo "$GOAL"
|
|
247
|
+
fi
|
|
248
|
+
echo
|
|
249
|
+
echo "## Where things stand"
|
|
250
|
+
echo
|
|
251
|
+
echo "$DONE_STORIES of $TOTAL_STORIES stories are complete."
|
|
252
|
+
if [ -n "$DONE_LIST" ]; then
|
|
253
|
+
echo
|
|
254
|
+
echo "Already done:"
|
|
255
|
+
echo
|
|
256
|
+
echo "$DONE_LIST"
|
|
257
|
+
fi
|
|
258
|
+
echo
|
|
259
|
+
echo "## The next three steps"
|
|
260
|
+
echo
|
|
261
|
+
if [ -n "$NEXT_STEPS" ]; then
|
|
262
|
+
echo "$NEXT_STEPS"
|
|
263
|
+
else
|
|
264
|
+
echo "All stories are complete. Remaining work, if any, is described in the project's post-implementation notes."
|
|
265
|
+
fi
|
|
266
|
+
if [ -n "$OPEN_QUESTIONS" ]; then
|
|
267
|
+
echo "## Open questions"
|
|
268
|
+
echo
|
|
269
|
+
echo "$OPEN_QUESTIONS"
|
|
270
|
+
echo
|
|
271
|
+
fi
|
|
272
|
+
if [ -n "$TRAPS" ]; then
|
|
273
|
+
echo "## Known traps and conventions"
|
|
274
|
+
echo
|
|
275
|
+
echo "$TRAPS"
|
|
276
|
+
echo
|
|
277
|
+
fi
|
|
278
|
+
echo "## Where everything lives"
|
|
279
|
+
echo
|
|
280
|
+
echo "The full PRD (every story, acceptance criteria, and decision history) is at \`companies/$COMPANY/projects/$PROJECT/prd.json\`. The pickup prompt that accompanied this brief pulls every file you need on demand — you do not need to run a full sync."
|
|
281
|
+
} > "$STAGE/BRIEF.md"
|
|
282
|
+
|
|
283
|
+
# --- fail-closed secret scan -------------------------------------------------
|
|
284
|
+
|
|
285
|
+
if ! hq_scan_secrets "$STAGE/manifest.json" "$STAGE/BRIEF.md"; then
|
|
286
|
+
die "generated bundle matched a secret-detection pattern — nothing written (see stderr above)"
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
# --- dry run: print the manifest, write nothing ------------------------------
|
|
290
|
+
|
|
291
|
+
if [ "$DRY_RUN" -eq 1 ]; then
|
|
292
|
+
cat "$STAGE/manifest.json"
|
|
293
|
+
rm -rf "$STAGE"
|
|
294
|
+
trap - EXIT
|
|
295
|
+
echo "hq-delegate-bundle: dry run — nothing written" >&2
|
|
296
|
+
exit 0
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
# --- move into place ---------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
DEST="$HQ_ROOT/workspace/delegations/$DELEGATION_ID"
|
|
302
|
+
mkdir -p "$(dirname "$DEST")"
|
|
303
|
+
mv "$STAGE" "$DEST"
|
|
304
|
+
trap - EXIT
|
|
305
|
+
|
|
306
|
+
echo "$DELEGATION_ID"
|
|
307
|
+
echo "hq-delegate-bundle: wrote workspace/delegations/$DELEGATION_ID/{manifest.json,BRIEF.md}" >&2
|
|
@@ -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'
|