@indigoai-us/hq-cli 5.291.0 → 5.292.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +27 -8
  2. package/assets/scaffold/core/scripts/handoff-finalize.sh +18 -0
  3. package/assets/scaffold/core/scripts/handoff-post.sh +71 -6
  4. package/assets/scaffold/core/scripts/hq-delegate-bundle.sh +307 -0
  5. package/assets/scaffold/core/scripts/hq-delegate-grant.sh +226 -0
  6. package/assets/scaffold/core/scripts/hq-delegate-publish.sh +91 -0
  7. package/assets/scaffold/core/scripts/hq-delegate-send.sh +259 -0
  8. package/assets/scaffold/core/scripts/hq-delegate-verify.sh +213 -0
  9. package/assets/scaffold/core/scripts/hq-job-notify.sh +389 -0
  10. package/assets/scaffold/core/scripts/hq-job-probe.sh +835 -0
  11. package/assets/scaffold/core/scripts/hq-job-remediate.sh +359 -0
  12. package/assets/scaffold/core/scripts/hq-job-run.sh +1025 -0
  13. package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +830 -0
  14. package/dist/command-catalog.generated.d.ts +106 -1
  15. package/dist/command-catalog.generated.js +132 -1
  16. package/dist/commands/core-delegate-w1b.d.ts +8 -0
  17. package/dist/commands/core-delegate-w1b.js +1049 -0
  18. package/dist/commands/core-delegate.d.ts +28 -0
  19. package/dist/commands/core-delegate.js +15 -13
  20. package/dist/commands/core-handoff.js +152 -23
  21. package/dist/commands/core-jobs-native.d.ts +15 -0
  22. package/dist/commands/core-jobs-native.js +709 -2
  23. package/dist/commands/core-ontology.d.ts +22 -4
  24. package/dist/commands/core-ontology.js +90 -8
  25. package/dist/commands/core.js +10 -0
  26. package/dist/lib/checkpoint-ontology/drop-aliases.d.ts +41 -0
  27. package/dist/lib/checkpoint-ontology/drop-aliases.js +76 -0
  28. package/dist/lib/checkpoint-ontology/entity-file.d.ts +6 -3
  29. package/dist/lib/checkpoint-ontology/entity-file.js +12 -6
  30. package/dist/lib/checkpoint-ontology/merge-copies.d.ts +27 -8
  31. package/dist/lib/checkpoint-ontology/merge-copies.js +64 -97
  32. package/dist/lib/daemon/flag.d.ts +2 -0
  33. package/dist/lib/daemon/flag.js +2 -0
  34. package/dist/utils/scaffold-sibling-references.js +4 -4
  35. package/dist/utils/self-update.d.ts +6 -0
  36. package/dist/utils/self-update.js +28 -6
  37. package/dist/utils/staged-install.d.ts +34 -0
  38. package/dist/utils/staged-install.js +296 -0
  39. package/package.json +1 -1
  40. package/scripts/mesh-daemon-if-stale.mjs +4 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.292.0] — 2026-09-30
6
+ - Add native `hq core jobs reconcile` behind `core-native-jobs`, retaining the bundled shell rollback and scheduled-job helpers.
7
+ - Behind the default-off `cli.staged-self-update` flag, an npm global self-update installs the new version beside the running package, checks that it answers `--version`, then swaps it in with two renames. The running `hq` stays on disk for the whole install, so services the daemon starts during an update no longer fail with "hq: command not found" or MODULE_NOT_FOUND. pnpm, Bun, Windows and linked installs keep the in-place update.
8
+
9
+ - Disable Git auto-maintenance, auto-gc, and detached gc during CM-W2b corpus
10
+ creation so fixture snapshots cannot race lock files.
11
+
12
+ ### Added
13
+
14
+ - `hq core ontology merge-copies --listed --only <file>` (flag `checkpoint.ontology.merge-listed-pairs`, default off) merges exactly the copy -> original pairs in the file, including pairs whose names differ, such as a company listed under two names. Each pair must name two entities of one type; a copy listed twice, or listed as both a copy and an original, is skipped as `listed_conflict`. Signal and link holds apply as for other pairs.
15
+ - `hq core ontology drop-aliases --only <file>` (flag `checkpoint.ontology.drop-aliases`, default off) removes reviewed wrong aliases, given as `{ entity, alias }` pairs, from entity files. It is a dry run unless `--apply` is given, and it writes through the multi-writer outbox so sync uploads the change.
16
+
17
+ ### Fixed
18
+
19
+ - `hq core ontology merge-copies --rewrite-refs` no longer edits signal or source files, which sync cannot upload. A pair whose copy a signal or source still names is now held as `server_owned_refs`, and its copy is kept.
20
+ ### Added
21
+
22
+ - Add `hq core delegate bundle`, `grant`, `publish`, `send`, and `verify` as
23
+ native TypeScript handlers behind the default-off `core-native-delegate`
24
+ flag, with their shell companions retained as rollback assets.
25
+ - Match native handoff open-steps JSON nulls and invalid-JSON exit codes, and sync zero-change handoffs to the calling session's bound company after mirroring the thread.
26
+
5
27
  ## [5.291.0] — 2026-09-29
6
28
  - Add native `hq core jobs schedule-parse` and `validate` commands behind the default-off `core-native-jobs` gate, retaining their shell rollback companions.
7
29
 
@@ -28,14 +50,6 @@
28
50
  ### Added
29
51
 
30
52
  - Gate `hq sync push --personal <paths>` behind the default-off `cli.personal-targeted-push-paths` flag; explicit paths are normalized and checked against personal-vault scope and exclusions before upload.
31
-
32
- ## [5.289.0] — 2026-09-29
33
- - A failed `hq update` restores the previous working hq-cli install when it could be backed up, instead of leaving the CLI partially updated.
34
-
35
- ## [5.288.0] — 2026-09-29
36
-
37
- ### Added
38
-
39
53
  - `hq core ontology merge-copies --rewrite-refs` (flag `checkpoint.ontology.merge-rewrite-refs`, default off) re-points signal refs, invoice and bill links, source and knowledge links from a merged copy to its survivor and carries the copy's external ids and web domains; a pair whose files hold different values for one external id stays held (`external_id_conflict`).
40
54
  - A `--only` entry can carry `resolve_external_ids: { <provider>: { primary, namespace, object_type? } }`. With `--rewrite-refs`, a conflicting provider id then keeps `primary` in `external_ids` and moves the pair's other id to the survivor's `external_refs` (`system`, `namespace`, `objectType`, `recordId`), so no id is dropped. A `primary` that matches neither id still holds the pair.
41
55
 
@@ -47,6 +61,11 @@
47
61
  - `merge-copies --rewrite-refs` no longer holds a survivor as `merge_invalid` when it gains external ids but has no `web_domains` key (or the reverse).
48
62
  - `merge-copies` treats an annotation list item whose first key is `path:` as a link to the copy, so it no longer deletes a copy such an annotation still names.
49
63
 
64
+ ## [5.289.0] — 2026-09-29
65
+ - A failed `hq update` restores the previous working hq-cli install when it could be backed up, instead of leaving the CLI partially updated.
66
+
67
+ ## [5.288.0] — 2026-09-29
68
+
50
69
  ### Added
51
70
 
52
71
  - Add lead autopilot prompts to the lanes reconcile pass behind
@@ -56,6 +56,8 @@ set -euo pipefail
56
56
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
57
57
  cd "$HQ_ROOT"
58
58
  HQ_ROOT="$(pwd -P)"
59
+ HANDOFF_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
60
+ source "$HANDOFF_SCRIPT_DIR/lib/session-id.sh"
59
61
  HAS_GIT_CHECKOUT="false"
60
62
  COMMON_GIT_DIR=""
61
63
  GIT_PROBE_OUTPUT=""
@@ -252,6 +254,22 @@ MIRROR_COMPANIES_JSON=$(printf '%s\n' "${SAFE_STAGE_PATHS[@]:-}" | jq -R -s -c '
252
254
  | unique
253
255
  ' 2>/dev/null || printf '[]')
254
256
 
257
+ bound_session_company() {
258
+ local session_id metadata company
259
+ session_id="$(session_id_resolve "$HQ_ROOT")"
260
+ [[ -n "$session_id" ]] || return 0
261
+ metadata="$HQ_ROOT/workspace/sessions/$session_id/meta.yaml"
262
+ [[ -r "$metadata" ]] || return 0
263
+ company="$(awk '$1 == "company_slug:" { sub(/^[^:]+:[[:space:]]*/, ""); gsub(/^"|"$/, ""); print; exit }' "$metadata" 2>/dev/null || true)"
264
+ [[ "$company" =~ ^[a-z][a-z0-9_-]*$ ]] || return 0
265
+ printf '%s' "$company"
266
+ }
267
+
268
+ BOUND_SESSION_COMPANY="$(bound_session_company)"
269
+ if [[ -n "$BOUND_SESSION_COMPANY" && -d "companies/$BOUND_SESSION_COMPANY" ]]; then
270
+ MIRROR_COMPANIES_JSON="$(jq -c --arg company "$BOUND_SESSION_COMPANY" '. + [$company] | unique' <<< "$MIRROR_COMPANIES_JSON")"
271
+ fi
272
+
255
273
  # -------- bg git reap (if launched by caller) --------
256
274
  GIT_BG_ERRORS=""
257
275
  if [[ -f /tmp/handoff-git-bg.pid ]]; then
@@ -18,6 +18,8 @@ set -euo pipefail
18
18
 
19
19
  HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
20
20
  cd "$HQ_ROOT"
21
+ HANDOFF_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
22
+ source "$HANDOFF_SCRIPT_DIR/lib/session-id.sh"
21
23
 
22
24
  THREAD_PATH="${1:-}"
23
25
  LEARNINGS_FILE="${2:-}"
@@ -105,18 +107,83 @@ fi
105
107
 
106
108
  # --- 3b. Push company handoff mirrors promptly (best effort) ---
107
109
  # Session snapshots are intentionally gitignored, so a completed mirror needs
108
- # the sync client to make the handoff available to a second device. Trust only
109
- # the finalizer's lowercase company-slug metadata before constructing a path.
110
+ # the sync client to make the handoff available to a second device. Keep
111
+ # finalizer company metadata and add the company bound to this session when no
112
+ # changed path identified one; never use a device-wide active-company value.
113
+ bound_session_company() {
114
+ local session_id metadata company
115
+ session_id="$(session_id_resolve "$HQ_ROOT")"
116
+ [[ -n "$session_id" ]] || return 0
117
+ metadata="$HQ_ROOT/workspace/sessions/$session_id/meta.yaml"
118
+ [[ -r "$metadata" ]] || return 0
119
+ company="$(awk '$1 == "company_slug:" { sub(/^[^:]+:[[:space:]]*/, ""); gsub(/^"|"$/, ""); print; exit }' "$metadata" 2>/dev/null || true)"
120
+ [[ "$company" =~ ^[a-z][a-z0-9_-]*$ ]] || return 0
121
+ printf '%s' "$company"
122
+ }
123
+
110
124
  if [[ -n "$THREAD_PATH" && -f "$THREAD_PATH" ]] && command -v hq >/dev/null 2>&1; then
125
+ if [[ "$THREAD_PATH" == /* ]]; then
126
+ THREAD_FILE="$THREAD_PATH"
127
+ else
128
+ THREAD_FILE="$HQ_ROOT/$THREAD_PATH"
129
+ fi
130
+ SYNC_COMPANIES_JSON="$(jq -c '
131
+ (.metadata.company // [])
132
+ | (if type == "array" then . else [.] end)
133
+ | map(select(type == "string" and test("^[a-z][a-z0-9_-]*$")))
134
+ | unique
135
+ ' "$THREAD_FILE" 2>/dev/null || printf '[]')"
136
+ BOUND_COMPANY="$(bound_session_company)"
137
+ if [[ -n "$BOUND_COMPANY" ]]; then
138
+ UPDATED_SYNC_COMPANIES_JSON="$(jq -c --arg company "$BOUND_COMPANY" '. + [$company] | unique' <<< "$SYNC_COMPANIES_JSON")"
139
+ if [[ "$SYNC_COMPANIES_JSON" != "$UPDATED_SYNC_COMPANIES_JSON" ]]; then
140
+ SYNC_COMPANIES_JSON="$UPDATED_SYNC_COMPANIES_JSON"
141
+ THREAD_TMP="$(mktemp "$HQ_ROOT/workspace/threads/.handoff-post-XXXXXX")"
142
+ if jq -c --argjson companies "$SYNC_COMPANIES_JSON" \
143
+ '.metadata = (.metadata // {}) | .metadata.company = $companies' \
144
+ "$THREAD_FILE" > "$THREAD_TMP" && mv "$THREAD_TMP" "$THREAD_FILE"; then
145
+ :
146
+ else
147
+ rm -f "$THREAD_TMP"
148
+ log "workspace-sync: skipped (could not add bound company to handoff thread)"
149
+ SYNC_COMPANIES_JSON='[]'
150
+ fi
151
+ else
152
+ SYNC_COMPANIES_JSON="$UPDATED_SYNC_COMPANIES_JSON"
153
+ fi
154
+ fi
155
+ THREAD_ID="$(jq -r '.thread_id // empty' "$THREAD_FILE" 2>/dev/null || true)"
156
+ THREAD_MIRRORABLE="false"
157
+ case "$THREAD_FILE" in
158
+ "$HQ_ROOT"/workspace/threads/T-*.json)
159
+ if [[ "$THREAD_ID" =~ ^T-[A-Za-z0-9._-]+$ ]]; then THREAD_MIRRORABLE="true"; fi
160
+ ;;
161
+ esac
162
+ if [[ "$THREAD_MIRRORABLE" == "true" && "$SYNC_COMPANIES_JSON" != '[]' ]]; then
163
+ MIRROR_HOOK="$HQ_ROOT/.claude/hooks/mirror-thread-to-company.sh"
164
+ if [[ -f "$MIRROR_HOOK" ]]; then
165
+ jq -n --arg file_path "$THREAD_FILE" \
166
+ '{tool_name:"Write",tool_input:{file_path:$file_path}}' | \
167
+ bash "$MIRROR_HOOK" >>"$LOG_MAIN" 2>&1 || \
168
+ log "workspace-sync: mirror hook failed for handoff thread"
169
+ else
170
+ log "workspace-sync: mirror hook unavailable for handoff thread"
171
+ fi
172
+ fi
111
173
  while IFS= read -r company; do
112
174
  [[ "$company" =~ ^[a-z][a-z0-9_-]*$ ]] || continue
113
175
  [[ -d "$HQ_ROOT/companies/$company/workspace" ]] || continue
176
+ if [[ "$THREAD_MIRRORABLE" != "true" || \
177
+ ! -f "$HQ_ROOT/companies/$company/workspace/sessions/$THREAD_ID.json" ]]; then
178
+ log "workspace-sync: skipped (thread mirror missing for $company)"
179
+ continue
180
+ fi
114
181
  if hq sync push --company "$company" "companies/$company/workspace" >>"$LOG_MAIN" 2>&1; then
115
182
  log "workspace-sync: pushed companies/$company/workspace"
116
183
  else
117
184
  log "workspace-sync: failed companies/$company/workspace"
118
185
  fi
119
- done < <(jq -r '.metadata.company // empty | if type == "array" then .[] else . end' "$THREAD_PATH" 2>/dev/null || true)
186
+ done < <(jq -r '.[]' <<< "$SYNC_COMPANIES_JSON")
120
187
  elif [[ -n "$THREAD_PATH" && -f "$THREAD_PATH" ]]; then
121
188
  log "workspace-sync: skipped (hq CLI unavailable)"
122
189
  fi
@@ -150,9 +217,7 @@ fi
150
217
  WM_END_HOOK="$HQ_ROOT/core/hooks/SessionEnd/35-work-mesh-session-end.sh"
151
218
  if [[ -f "$WM_END_HOOK" ]]; then
152
219
  sid=""
153
- if [[ -f "$HQ_ROOT/workspace/sessions/.current" ]]; then
154
- sid="$(tr -d '[:space:]' <"$HQ_ROOT/workspace/sessions/.current" 2>/dev/null || true)"
155
- fi
220
+ sid="$(session_id_resolve "$HQ_ROOT")"
156
221
  if [[ -n "$sid" ]]; then
157
222
  payload=$(printf '{"session_id":"%s"}' "$sid")
158
223
  else
@@ -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