@indigoai-us/hq-cli 5.290.0 → 5.292.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +66 -3
  2. package/assets/scaffold/core/scripts/handoff-finalize.sh +18 -0
  3. package/assets/scaffold/core/scripts/handoff-post.sh +71 -6
  4. package/assets/scaffold/core/scripts/hook-lib.sh +345 -50
  5. package/assets/scaffold/core/scripts/hq-delegate-bundle.sh +307 -0
  6. package/assets/scaffold/core/scripts/hq-delegate-grant.sh +226 -0
  7. package/assets/scaffold/core/scripts/hq-delegate-publish.sh +91 -0
  8. package/assets/scaffold/core/scripts/hq-delegate-send.sh +259 -0
  9. package/assets/scaffold/core/scripts/hq-delegate-verify.sh +213 -0
  10. package/assets/scaffold/core/scripts/hq-job-notify.sh +389 -0
  11. package/assets/scaffold/core/scripts/hq-job-probe.sh +835 -0
  12. package/assets/scaffold/core/scripts/hq-job-remediate.sh +359 -0
  13. package/assets/scaffold/core/scripts/hq-job-run.sh +1025 -0
  14. package/assets/scaffold/core/scripts/hq-session.sh +182 -21
  15. package/assets/scaffold/core/scripts/jobs-schedule-parse.sh +310 -0
  16. package/assets/scaffold/core/scripts/jobs-validate.sh +451 -0
  17. package/assets/scaffold/core/scripts/lib/session-id.sh +3 -2
  18. package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +2 -1
  19. package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +830 -0
  20. package/dist/command-catalog.generated.d.ts +165 -1
  21. package/dist/command-catalog.generated.js +205 -1
  22. package/dist/commands/cloud.d.ts +26 -14
  23. package/dist/commands/cloud.js +207 -56
  24. package/dist/commands/core-delegate-w1b.d.ts +8 -0
  25. package/dist/commands/core-delegate-w1b.js +1049 -0
  26. package/dist/commands/core-delegate.d.ts +28 -0
  27. package/dist/commands/core-delegate.js +15 -13
  28. package/dist/commands/core-handoff.js +152 -23
  29. package/dist/commands/core-jobs-native.d.ts +23 -0
  30. package/dist/commands/core-jobs-native.js +1331 -0
  31. package/dist/commands/core-native-jobs-flag.d.ts +4 -0
  32. package/dist/commands/core-native-jobs-flag.js +7 -0
  33. package/dist/commands/core-ontology.d.ts +22 -4
  34. package/dist/commands/core-ontology.js +90 -8
  35. package/dist/commands/core-policy.js +24 -5
  36. package/dist/commands/core.js +17 -0
  37. package/dist/commands/integrations-core.js +7 -0
  38. package/dist/commands/lanes.js +111 -35
  39. package/dist/commands/members.js +5 -0
  40. package/dist/commands/mesh.js +46 -25
  41. package/dist/commands/personal-targeted-push-flag.d.ts +4 -0
  42. package/dist/commands/personal-targeted-push-flag.js +7 -0
  43. package/dist/commands/personal-targeted-push-paths.d.ts +3 -0
  44. package/dist/commands/personal-targeted-push-paths.js +67 -0
  45. package/dist/commands/secrets.js +9 -0
  46. package/dist/commands/skill.d.ts +3 -0
  47. package/dist/commands/skill.js +28 -0
  48. package/dist/fast-core.js +1 -0
  49. package/dist/lib/billing/free-month.d.ts +18 -0
  50. package/dist/lib/billing/free-month.js +31 -0
  51. package/dist/lib/billing/plan-limit-refusals.d.ts +69 -0
  52. package/dist/lib/billing/plan-limit-refusals.js +187 -0
  53. package/dist/lib/billing/plan-lock.d.ts +2 -6
  54. package/dist/lib/billing/plan-lock.js +5 -16
  55. package/dist/lib/checkpoint-ontology/drop-aliases.d.ts +41 -0
  56. package/dist/lib/checkpoint-ontology/drop-aliases.js +76 -0
  57. package/dist/lib/checkpoint-ontology/entity-file.d.ts +6 -3
  58. package/dist/lib/checkpoint-ontology/entity-file.js +12 -6
  59. package/dist/lib/checkpoint-ontology/merge-copies.d.ts +27 -8
  60. package/dist/lib/checkpoint-ontology/merge-copies.js +64 -97
  61. package/dist/lib/core-utils/secret-patterns.d.ts +3 -1
  62. package/dist/lib/core-utils/secret-patterns.js +3 -2
  63. package/dist/lib/daemon/flag.d.ts +2 -0
  64. package/dist/lib/daemon/flag.js +2 -0
  65. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  66. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  67. package/dist/lib/lanes/autopilot.d.ts +69 -0
  68. package/dist/lib/lanes/autopilot.js +418 -0
  69. package/dist/lib/lanes/mesh-events.js +2 -0
  70. package/dist/lib/lanes/store.js +11 -6
  71. package/dist/lib/mesh/api.js +4 -0
  72. package/dist/lib/mesh/live/daemon/state.d.ts +7 -1
  73. package/dist/lib/mesh/live/daemon/state.js +34 -2
  74. package/dist/main.js +23 -12
  75. package/dist/utils/plan-gate-error.d.ts +24 -0
  76. package/dist/utils/plan-gate-error.js +219 -9
  77. package/dist/utils/scaffold-sibling-references.js +4 -4
  78. package/dist/utils/self-update.d.ts +6 -0
  79. package/dist/utils/self-update.js +28 -6
  80. package/dist/utils/staged-install.d.ts +34 -0
  81. package/dist/utils/staged-install.js +296 -0
  82. package/package.json +2 -2
  83. package/scripts/mesh-daemon-if-stale.mjs +4 -0
@@ -0,0 +1,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'