@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.
- package/CHANGELOG.md +27 -8
- package/assets/scaffold/core/scripts/handoff-finalize.sh +18 -0
- package/assets/scaffold/core/scripts/handoff-post.sh +71 -6
- package/assets/scaffold/core/scripts/hq-delegate-bundle.sh +307 -0
- package/assets/scaffold/core/scripts/hq-delegate-grant.sh +226 -0
- package/assets/scaffold/core/scripts/hq-delegate-publish.sh +91 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +259 -0
- package/assets/scaffold/core/scripts/hq-delegate-verify.sh +213 -0
- package/assets/scaffold/core/scripts/hq-job-notify.sh +389 -0
- package/assets/scaffold/core/scripts/hq-job-probe.sh +835 -0
- package/assets/scaffold/core/scripts/hq-job-remediate.sh +359 -0
- package/assets/scaffold/core/scripts/hq-job-run.sh +1025 -0
- package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +830 -0
- package/dist/command-catalog.generated.d.ts +106 -1
- package/dist/command-catalog.generated.js +132 -1
- package/dist/commands/core-delegate-w1b.d.ts +8 -0
- package/dist/commands/core-delegate-w1b.js +1049 -0
- package/dist/commands/core-delegate.d.ts +28 -0
- package/dist/commands/core-delegate.js +15 -13
- package/dist/commands/core-handoff.js +152 -23
- package/dist/commands/core-jobs-native.d.ts +15 -0
- package/dist/commands/core-jobs-native.js +709 -2
- package/dist/commands/core-ontology.d.ts +22 -4
- package/dist/commands/core-ontology.js +90 -8
- package/dist/commands/core.js +10 -0
- package/dist/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/daemon/flag.d.ts +2 -0
- package/dist/lib/daemon/flag.js +2 -0
- 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 +1 -1
- 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.
|
|
109
|
-
#
|
|
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 '.
|
|
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
|
-
|
|
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
|