@indigoai-us/hq-cli 5.289.0 → 5.291.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 +61 -0
- package/assets/scaffold/core/scripts/handoff-finalize.sh +670 -0
- package/assets/scaffold/core/scripts/handoff-open-steps.sh +81 -0
- package/assets/scaffold/core/scripts/handoff-post.sh +187 -0
- package/assets/scaffold/core/scripts/hook-lib.sh +345 -50
- 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/qmd-reindex-bg.sh +40 -0
- package/dist/command-catalog.generated.d.ts +110 -0
- package/dist/command-catalog.generated.js +136 -0
- package/dist/commands/cloud.d.ts +26 -14
- package/dist/commands/cloud.js +207 -56
- package/dist/commands/core-handoff.d.ts +22 -0
- package/dist/commands/core-handoff.js +1240 -0
- package/dist/commands/core-jobs-native.d.ts +8 -0
- package/dist/commands/core-jobs-native.js +624 -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 +6 -1
- package/dist/commands/core-ontology.js +42 -7
- package/dist/commands/core-policy.js +24 -5
- package/dist/commands/core.js +15 -1
- 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/entity-file.d.ts +21 -0
- package/dist/lib/checkpoint-ontology/entity-file.js +100 -0
- package/dist/lib/checkpoint-ontology/merge-copies.d.ts +34 -2
- package/dist/lib/checkpoint-ontology/merge-copies.js +541 -27
- package/dist/lib/core-utils/secret-patterns.d.ts +3 -1
- package/dist/lib/core-utils/secret-patterns.js +3 -2
- 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.d.ts +4 -2
- package/dist/lib/lanes/store.js +18 -9
- 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/self-update.d.ts +3 -0
- package/dist/utils/self-update.js +232 -1
- package/package.json +2 -2
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# handoff-finalize.sh — collapse the mechanical tail of /handoff into one call.
|
|
3
|
+
#
|
|
4
|
+
# Folds steps 1/2/3b/4/5/7 of the handoff skill into a single subprocess so the
|
|
5
|
+
# model doesn't narrate each step. Writes the thread file, handoff.json,
|
|
6
|
+
# regenerates INDEX files, commits HQ via explicit paths, and launches qmd
|
|
7
|
+
# reindex fire-and-forget via qmd-reindex-bg.sh (agent skip / laptop single-flight).
|
|
8
|
+
#
|
|
9
|
+
# Usage (all args optional):
|
|
10
|
+
# handoff-finalize.sh \
|
|
11
|
+
# --title "Handoff: brief description" \
|
|
12
|
+
# --summary "What was accomplished" \
|
|
13
|
+
# --message "User message or autogenerated" \
|
|
14
|
+
# --next-steps-json '["step 1","step 2"]' \
|
|
15
|
+
# --files-touched-json '["path1","path2"]' \
|
|
16
|
+
# --files-touched-json-file workspace/threads/.changeset.json \
|
|
17
|
+
# --learnings-json '[]' \
|
|
18
|
+
# --tags-json '["handoff"]' \
|
|
19
|
+
# --slug "handoff-short-tag" \
|
|
20
|
+
# --next-command "/startwork"
|
|
21
|
+
#
|
|
22
|
+
# The changeset (files-touched) may be supplied inline via --files-touched-json
|
|
23
|
+
# OR from a file via --files-touched-json-file <path>. Prefer the file form for
|
|
24
|
+
# large changesets: on Windows Git Bash, CreateProcess caps a command line at
|
|
25
|
+
# ~32KB (~8KB under cmd.exe), so a big inline value would overflow argv as it
|
|
26
|
+
# rides through hq-status-summary.sh and jq. The file form keeps the giant JSON
|
|
27
|
+
# off argv at every hop. --files-touched-json-file wins if both are given; a
|
|
28
|
+
# missing file path is a hard error (exit 3). Relative paths resolve against the
|
|
29
|
+
# HQ root.
|
|
30
|
+
#
|
|
31
|
+
# --next-command is copied to the user's clipboard fail-soft (pbcopy /
|
|
32
|
+
# wl-copy / xclip) so the follow-up command is ready to paste into a fresh
|
|
33
|
+
# session. Defaults to "/resumework {thread_id}". Pass "" to skip the copy.
|
|
34
|
+
#
|
|
35
|
+
# Outputs JSON on stdout:
|
|
36
|
+
# {"thread_id":"...","thread_path":"...","handoff_path":"...",
|
|
37
|
+
# "hq_committed":true,"hq_commit_status":"committed","hq_commit_error":"",
|
|
38
|
+
# "committed_paths":[...],"stage_failures":[],"indexes_regen":true,
|
|
39
|
+
# "qmd_pid":12345,"next_command":"/resumework T-...","clipboard_copied":true}
|
|
40
|
+
#
|
|
41
|
+
# hq_commit_status is the honest reason behind hq_committed:
|
|
42
|
+
# committed — the handoff commit landed; committed_paths lists it
|
|
43
|
+
# nothing-to-commit — no staged change existed; hq_committed:false, exit 0
|
|
44
|
+
# failed — git refused the commit; committed_paths is [],
|
|
45
|
+
# hq_commit_error carries git's message, exit 4
|
|
46
|
+
# stage-failed — git add rejected one or more paths, so the handoff is
|
|
47
|
+
# incomplete even if a commit landed for the rest; exit 4.
|
|
48
|
+
# This is the shape a held .git/index.lock takes.
|
|
49
|
+
# stage_failures lists paths git add rejected for a reason other than gitignore.
|
|
50
|
+
#
|
|
51
|
+
# Exit codes: 0 ok · 2 bad argument · 3 changeset file missing · 4 handoff not
|
|
52
|
+
# durable (commit refused or paths unstageable).
|
|
53
|
+
|
|
54
|
+
set -euo pipefail
|
|
55
|
+
|
|
56
|
+
HQ_ROOT="${HQ_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
|
57
|
+
cd "$HQ_ROOT"
|
|
58
|
+
HQ_ROOT="$(pwd -P)"
|
|
59
|
+
HAS_GIT_CHECKOUT="false"
|
|
60
|
+
COMMON_GIT_DIR=""
|
|
61
|
+
GIT_PROBE_OUTPUT=""
|
|
62
|
+
if GIT_PROBE_OUTPUT="$(LC_ALL=C git rev-parse --git-common-dir 2>&1)"; then
|
|
63
|
+
COMMON_GIT_DIR="$GIT_PROBE_OUTPUT"
|
|
64
|
+
HAS_GIT_CHECKOUT="true"
|
|
65
|
+
else
|
|
66
|
+
GIT_PROBE_STATUS=$?
|
|
67
|
+
if [[ ! -e "$HQ_ROOT/.git" && ! -L "$HQ_ROOT/.git" && \
|
|
68
|
+
"$GIT_PROBE_OUTPUT" == *"not a git repository"* ]]; then
|
|
69
|
+
COMMON_GIT_DIR=""
|
|
70
|
+
else
|
|
71
|
+
printf 'handoff-finalize: Git checkout probe failed (exit %s): %s\n' \
|
|
72
|
+
"$GIT_PROBE_STATUS" "$GIT_PROBE_OUTPUT" >&2
|
|
73
|
+
exit 4
|
|
74
|
+
fi
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# -------- args --------
|
|
78
|
+
TITLE="Handoff: session continuation"
|
|
79
|
+
SUMMARY=""
|
|
80
|
+
MESSAGE=""
|
|
81
|
+
NEXT_STEPS_JSON="[]"
|
|
82
|
+
FILES_TOUCHED_JSON="[]"
|
|
83
|
+
FILES_TOUCHED_JSON_FILE=""
|
|
84
|
+
LEARNINGS_JSON="[]"
|
|
85
|
+
TAGS_JSON='["handoff"]'
|
|
86
|
+
SLUG="handoff"
|
|
87
|
+
NEXT_COMMAND="__auto__"
|
|
88
|
+
|
|
89
|
+
while [[ $# -gt 0 ]]; do
|
|
90
|
+
case "$1" in
|
|
91
|
+
--title) TITLE="$2"; shift 2 ;;
|
|
92
|
+
--summary) SUMMARY="$2"; shift 2 ;;
|
|
93
|
+
--message) MESSAGE="$2"; shift 2 ;;
|
|
94
|
+
--next-steps-json) NEXT_STEPS_JSON="$2"; shift 2 ;;
|
|
95
|
+
--files-touched-json) FILES_TOUCHED_JSON="$2"; shift 2 ;;
|
|
96
|
+
--files-touched-json-file) FILES_TOUCHED_JSON_FILE="$2"; shift 2 ;;
|
|
97
|
+
--learnings-json) LEARNINGS_JSON="$2"; shift 2 ;;
|
|
98
|
+
--tags-json) TAGS_JSON="$2"; shift 2 ;;
|
|
99
|
+
--slug) SLUG="$2"; shift 2 ;;
|
|
100
|
+
--next-command) NEXT_COMMAND="$2"; shift 2 ;;
|
|
101
|
+
*) echo "Unknown arg: $1" >&2; exit 2 ;;
|
|
102
|
+
esac
|
|
103
|
+
done
|
|
104
|
+
|
|
105
|
+
# -------- temp-file registry for off-argv JSON --------
|
|
106
|
+
# The Windows argv-overflow fix: any JSON value that scales with the changeset
|
|
107
|
+
# (the changeset itself, and the staged/skipped/committed path lists and status
|
|
108
|
+
# summary derived from it) is written to a temp file and fed to jq via
|
|
109
|
+
# --slurpfile, never via --argjson/--arg on the command line. CreateProcess caps
|
|
110
|
+
# a command line at ~32KB on Git Bash; even POSIX hosts cap a single argument at
|
|
111
|
+
# 128KB (MAX_ARG_STRLEN). A few-thousand-path changeset breaches both. Small,
|
|
112
|
+
# bounded values (title, ids, tags, company slugs, counts) stay inline.
|
|
113
|
+
#
|
|
114
|
+
# Temps live under workspace/threads/.handoff-tmp/ (gitignored) — not beside
|
|
115
|
+
# durable thread JSON — so they never flip git.dirty or baseline_noise before
|
|
116
|
+
# the status-summary call. Do not use /tmp (missing on some Windows Git Bash).
|
|
117
|
+
HANDOFF_TMP_DIR="workspace/threads/.handoff-tmp"
|
|
118
|
+
mkdir -p workspace/threads "$HANDOFF_TMP_DIR"
|
|
119
|
+
FINALIZE_TMP_FILES=()
|
|
120
|
+
cleanup_finalize_tmp() {
|
|
121
|
+
local f
|
|
122
|
+
for f in "${FINALIZE_TMP_FILES[@]:-}"; do
|
|
123
|
+
[[ -n "$f" ]] && rm -f "$f"
|
|
124
|
+
done
|
|
125
|
+
return 0
|
|
126
|
+
}
|
|
127
|
+
trap cleanup_finalize_tmp EXIT
|
|
128
|
+
|
|
129
|
+
# new_tmp NAME: create a registered temp file under .handoff-tmp/ (Git Bash safe;
|
|
130
|
+
# not /tmp) and assign its absolute path to NAME in the *current* shell.
|
|
131
|
+
# Callers must not use VAR=$(new_tmp) — command substitution runs in a subshell,
|
|
132
|
+
# so FINALIZE_TMP_FILES+= would never reach the EXIT trap and temps would leak.
|
|
133
|
+
new_tmp() {
|
|
134
|
+
local __outvar="$1"
|
|
135
|
+
local t
|
|
136
|
+
[[ -n "$__outvar" ]] || { echo "handoff-finalize: new_tmp requires a varname" >&2; exit 3; }
|
|
137
|
+
t=$(mktemp "$HANDOFF_TMP_DIR/.handoff-XXXXXX")
|
|
138
|
+
case "$t" in /*) : ;; *) t="$PWD/$t" ;; esac
|
|
139
|
+
FINALIZE_TMP_FILES+=("$t")
|
|
140
|
+
printf -v "$__outvar" '%s' "$t"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# -------- resolve changeset source to a single on-disk file --------
|
|
144
|
+
# Every downstream consumer of the changeset (path extraction, hq-status-summary,
|
|
145
|
+
# and the two jq emitters) reads it from FILES_TOUCHED_FILE, never from argv.
|
|
146
|
+
# --files-touched-json-file <path> wins over inline --files-touched-json; the
|
|
147
|
+
# inline form is materialized to a temp file so the read path is uniform.
|
|
148
|
+
if [[ -n "$FILES_TOUCHED_JSON_FILE" ]]; then
|
|
149
|
+
if [[ ! -f "$FILES_TOUCHED_JSON_FILE" ]]; then
|
|
150
|
+
echo "handoff-finalize: --files-touched-json-file not found: $FILES_TOUCHED_JSON_FILE" >&2
|
|
151
|
+
exit 3
|
|
152
|
+
fi
|
|
153
|
+
FILES_TOUCHED_FILE="$FILES_TOUCHED_JSON_FILE"
|
|
154
|
+
case "$FILES_TOUCHED_FILE" in
|
|
155
|
+
/*) : ;;
|
|
156
|
+
*) FILES_TOUCHED_FILE="$PWD/$FILES_TOUCHED_FILE" ;;
|
|
157
|
+
esac
|
|
158
|
+
else
|
|
159
|
+
new_tmp FILES_TOUCHED_FILE
|
|
160
|
+
printf '%s' "$FILES_TOUCHED_JSON" > "$FILES_TOUCHED_FILE"
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
# -------- changeset path extraction / validation --------
|
|
164
|
+
# Skipped paths accumulate as NDJSON on disk from the first record_skip — never
|
|
165
|
+
# rebuild a growing JSON document via jq argv/here-string (thousands of skips
|
|
166
|
+
# would hit Windows CreateProcess / Linux MAX_ARG_STRLEN before emit).
|
|
167
|
+
new_tmp SKIPPED_NDJSON_FILE
|
|
168
|
+
: > "$SKIPPED_NDJSON_FILE"
|
|
169
|
+
new_tmp SKIPPED_PATHS_FILE
|
|
170
|
+
printf '[]' > "$SKIPPED_PATHS_FILE"
|
|
171
|
+
SAFE_STAGE_PATHS=()
|
|
172
|
+
MIRROR_COMPANIES_JSON="[]"
|
|
173
|
+
MIRRORED_COMPANIES=()
|
|
174
|
+
MIRRORED_COMPANIES_JSON="[]"
|
|
175
|
+
MIRROR_STAGE_PATHS=()
|
|
176
|
+
|
|
177
|
+
record_skip() {
|
|
178
|
+
local path="$1"
|
|
179
|
+
local reason="$2"
|
|
180
|
+
jq -nc --arg path "$path" --arg reason "$reason" \
|
|
181
|
+
'{path: $path, reason: $reason}' >> "$SKIPPED_NDJSON_FILE"
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
is_sensitive_path() {
|
|
185
|
+
local p="$1"
|
|
186
|
+
case "$p" in
|
|
187
|
+
core/settings/.gitkeep|core/settings/pure-ralph.json)
|
|
188
|
+
return 1
|
|
189
|
+
;;
|
|
190
|
+
.git|.git/*|.env|.env.*|*.credentials.json|*.secret.*|.hq/deploy-passwords.json|.hq/cognito-tokens.json|.claude/settings.local.json|.claude/settings.local.*|companies/*/settings/*|settings/*.json|settings/**/*.json|core/settings/*.json|core/settings/**/*.json)
|
|
191
|
+
return 0
|
|
192
|
+
;;
|
|
193
|
+
*)
|
|
194
|
+
return 1
|
|
195
|
+
;;
|
|
196
|
+
esac
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
normalize_rel_path() {
|
|
200
|
+
local raw="$1"
|
|
201
|
+
local rel="$raw"
|
|
202
|
+
if [[ "$raw" == "$HQ_ROOT/"* ]]; then
|
|
203
|
+
rel="${raw#"$HQ_ROOT/"}"
|
|
204
|
+
elif [[ "$raw" == /* ]]; then
|
|
205
|
+
return 1
|
|
206
|
+
fi
|
|
207
|
+
rel="${rel#./}"
|
|
208
|
+
[[ -z "$rel" ]] && return 1
|
|
209
|
+
[[ "$rel" == -* || "$rel" == ".." || "$rel" == ../* || "$rel" == */../* || "$rel" == .git || "$rel" == .git/* ]] && return 1
|
|
210
|
+
printf '%s\n' "$rel"
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
while IFS=$'\t' read -r raw_path deleted_flag; do
|
|
214
|
+
[[ -z "${raw_path:-}" ]] && continue
|
|
215
|
+
if ! rel_path=$(normalize_rel_path "$raw_path"); then
|
|
216
|
+
record_skip "$raw_path" "unsafe-path"
|
|
217
|
+
continue
|
|
218
|
+
fi
|
|
219
|
+
if is_sensitive_path "$rel_path"; then
|
|
220
|
+
record_skip "$rel_path" "sensitive-path"
|
|
221
|
+
continue
|
|
222
|
+
fi
|
|
223
|
+
if [[ ! -e "$rel_path" ]]; then
|
|
224
|
+
if [[ "$HAS_GIT_CHECKOUT" == "true" && "$deleted_flag" == "true" ]] \
|
|
225
|
+
&& git ls-files --error-unmatch "$rel_path" >/dev/null 2>&1; then
|
|
226
|
+
SAFE_STAGE_PATHS+=("$rel_path")
|
|
227
|
+
else
|
|
228
|
+
record_skip "$rel_path" "missing"
|
|
229
|
+
fi
|
|
230
|
+
continue
|
|
231
|
+
fi
|
|
232
|
+
SAFE_STAGE_PATHS+=("$rel_path")
|
|
233
|
+
done < <(
|
|
234
|
+
jq -r '
|
|
235
|
+
if type != "array" then empty else
|
|
236
|
+
.[] |
|
|
237
|
+
if type == "string" then
|
|
238
|
+
[., "false"]
|
|
239
|
+
else
|
|
240
|
+
[(.path // ""), (((.deleted == true) or (.status == "deleted")) | tostring)]
|
|
241
|
+
end | @tsv
|
|
242
|
+
end
|
|
243
|
+
' "$FILES_TOUCHED_FILE" 2>/dev/null || true
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Company workspace mirrors are scoped from the already-validated changeset,
|
|
247
|
+
# never from ambient repository state. A handoff can legitimately touch more
|
|
248
|
+
# than one company, so preserve all unique slugs in durable thread metadata.
|
|
249
|
+
MIRROR_COMPANIES_JSON=$(printf '%s\n' "${SAFE_STAGE_PATHS[@]:-}" | jq -R -s -c '
|
|
250
|
+
split("\n")
|
|
251
|
+
| [ .[] | select(length > 0) | try (capture("^companies/(?<company>[a-z][a-z0-9_-]*)/").company) ]
|
|
252
|
+
| unique
|
|
253
|
+
' 2>/dev/null || printf '[]')
|
|
254
|
+
|
|
255
|
+
# -------- bg git reap (if launched by caller) --------
|
|
256
|
+
GIT_BG_ERRORS=""
|
|
257
|
+
if [[ -f /tmp/handoff-git-bg.pid ]]; then
|
|
258
|
+
PID=$(cat /tmp/handoff-git-bg.pid 2>/dev/null || true)
|
|
259
|
+
if [[ -n "$PID" ]]; then
|
|
260
|
+
wait "$PID" 2>/dev/null || true
|
|
261
|
+
if [[ -f /tmp/handoff-git-bg.log ]]; then
|
|
262
|
+
GIT_BG_ERRORS=$(grep '^ERR:' /tmp/handoff-git-bg.log 2>/dev/null || true)
|
|
263
|
+
fi
|
|
264
|
+
fi
|
|
265
|
+
rm -f /tmp/handoff-git-bg.pid
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
# -------- thread file --------
|
|
269
|
+
TS=$(date -u +%Y%m%dT%H%M%SZ)
|
|
270
|
+
DAY=$(date +%Y%m%d)
|
|
271
|
+
HM=$(date +%H%M%S)
|
|
272
|
+
THREAD_ID="T-${DAY}-${HM}-${SLUG}"
|
|
273
|
+
THREAD_PATH="workspace/threads/${THREAD_ID}.json"
|
|
274
|
+
CHANGESET_PATH="workspace/threads/${THREAD_ID}.changeset.json"
|
|
275
|
+
|
|
276
|
+
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "DETACHED")
|
|
277
|
+
START_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
278
|
+
DIRTY="false"
|
|
279
|
+
[[ -n "$(git status --porcelain 2>/dev/null)" ]] && DIRTY="true"
|
|
280
|
+
|
|
281
|
+
# Status summary and staged-path list both scale with the changeset / repo
|
|
282
|
+
# noise, so they are captured to files and later slurped into jq off argv.
|
|
283
|
+
new_tmp STATUS_SUMMARY_FILE
|
|
284
|
+
printf '{}' > "$STATUS_SUMMARY_FILE"
|
|
285
|
+
if command -v hq >/dev/null 2>&1; then
|
|
286
|
+
hq core hq-status-summary --session-files-file "$FILES_TOUCHED_FILE" --json > "$STATUS_SUMMARY_FILE" 2>/dev/null \
|
|
287
|
+
|| printf '{}' > "$STATUS_SUMMARY_FILE"
|
|
288
|
+
# Guard against a truncated/empty write masquerading as success.
|
|
289
|
+
[[ -s "$STATUS_SUMMARY_FILE" ]] || printf '{}' > "$STATUS_SUMMARY_FILE"
|
|
290
|
+
fi
|
|
291
|
+
BASELINE_NOISE_COUNT=$(jq -r '.counts.baseline_noise // 0' "$STATUS_SUMMARY_FILE" 2>/dev/null || echo 0)
|
|
292
|
+
new_tmp STAGED_PATHS_FILE
|
|
293
|
+
if [[ "$HAS_GIT_CHECKOUT" == "true" ]]; then
|
|
294
|
+
printf '%s\n' "${SAFE_STAGE_PATHS[@]:-}" | jq -R -s 'split("\n") | map(select(length > 0))' > "$STAGED_PATHS_FILE"
|
|
295
|
+
else
|
|
296
|
+
printf '[]' > "$STAGED_PATHS_FILE"
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
mkdir -p workspace/threads
|
|
300
|
+
# Every next step carries an id and a closure status (2026-09-07). Handoffs
|
|
301
|
+
# used to record steps with no way to close one; an audit found a task copied
|
|
302
|
+
# forward through three consecutive handoffs and never started. Ids are
|
|
303
|
+
# `<thread_id>#<n>`; status starts "open" and is closed by
|
|
304
|
+
# core/scripts/handoff-open-steps.sh close. Strings and {step:...} objects are
|
|
305
|
+
# both accepted; already-normalized objects keep their fields.
|
|
306
|
+
NEXT_STEPS_NORMALIZED_JSON="$(jq -c --arg tid "$THREAD_ID" '
|
|
307
|
+
to_entries | map(.key as $k |
|
|
308
|
+
(if (.value|type) == "string" then {step: .value} else .value end)
|
|
309
|
+
| .id = (.id // ($tid + "#" + (($k + 1)|tostring)))
|
|
310
|
+
| .status = (.status // "open")
|
|
311
|
+
) | map(del(.key))' <<<"$NEXT_STEPS_JSON" 2>/dev/null || printf '%s' "$NEXT_STEPS_JSON")"
|
|
312
|
+
|
|
313
|
+
jq -n \
|
|
314
|
+
--arg thread_id "$THREAD_ID" \
|
|
315
|
+
--arg ts "$TS" \
|
|
316
|
+
--arg root "$HQ_ROOT" \
|
|
317
|
+
--arg cwd "$(pwd)" \
|
|
318
|
+
--arg branch "$BRANCH" \
|
|
319
|
+
--arg commit "$START_COMMIT" \
|
|
320
|
+
--arg changeset_path "$CHANGESET_PATH" \
|
|
321
|
+
--argjson dirty "$DIRTY" \
|
|
322
|
+
--arg summary "$SUMMARY" \
|
|
323
|
+
--arg title "$TITLE" \
|
|
324
|
+
--argjson next_steps "$NEXT_STEPS_NORMALIZED_JSON" \
|
|
325
|
+
--slurpfile files_touched "$FILES_TOUCHED_FILE" \
|
|
326
|
+
--argjson learnings "$LEARNINGS_JSON" \
|
|
327
|
+
--argjson tags "$TAGS_JSON" \
|
|
328
|
+
--argjson companies "$MIRROR_COMPANIES_JSON" \
|
|
329
|
+
'{
|
|
330
|
+
thread_id: $thread_id,
|
|
331
|
+
version: 1,
|
|
332
|
+
type: "handoff",
|
|
333
|
+
created_at: $ts,
|
|
334
|
+
updated_at: $ts,
|
|
335
|
+
workspace_root: $root,
|
|
336
|
+
cwd: $cwd,
|
|
337
|
+
git: { branch: $branch, current_commit: $commit, dirty: $dirty },
|
|
338
|
+
changeset_path: $changeset_path,
|
|
339
|
+
conversation_summary: $summary,
|
|
340
|
+
next_steps: $next_steps,
|
|
341
|
+
files_touched: ($files_touched[0] // []),
|
|
342
|
+
learnings: $learnings,
|
|
343
|
+
metadata: { title: $title, tags: $tags, company: $companies }
|
|
344
|
+
}' > "$THREAD_PATH"
|
|
345
|
+
|
|
346
|
+
# `handoff-finalize.sh` writes from a shell subprocess, so its thread creation
|
|
347
|
+
# does not pass through editor PostToolUse hooks. Replay the installed mirror
|
|
348
|
+
# hook with Bash: hooks are source files and do not require an executable bit.
|
|
349
|
+
MIRROR_HOOK="$HQ_ROOT/.claude/hooks/mirror-thread-to-company.sh"
|
|
350
|
+
if [[ -f "$MIRROR_HOOK" ]]; then
|
|
351
|
+
jq -n --arg file_path "$HQ_ROOT/$THREAD_PATH" \
|
|
352
|
+
'{tool_name:"Write", tool_input:{file_path:$file_path}}' | \
|
|
353
|
+
bash "$MIRROR_HOOK" >/dev/null 2>&1 || true
|
|
354
|
+
fi
|
|
355
|
+
while IFS= read -r company; do
|
|
356
|
+
[[ -n "$company" ]] || continue
|
|
357
|
+
if [[ -f "companies/${company}/workspace/sessions/${THREAD_ID}.json" ]]; then
|
|
358
|
+
MIRRORED_COMPANIES+=("$company")
|
|
359
|
+
MIRROR_STAGE_PATHS+=(
|
|
360
|
+
"companies/${company}/workspace/index.jsonl"
|
|
361
|
+
"companies/${company}/workspace/.gitignore"
|
|
362
|
+
)
|
|
363
|
+
fi
|
|
364
|
+
done < <(jq -r '.[]' <<<"$MIRROR_COMPANIES_JSON")
|
|
365
|
+
MIRRORED_COMPANIES_JSON=$(printf '%s\n' "${MIRRORED_COMPANIES[@]:-}" | jq -R -s -c 'split("\n") | map(select(length > 0))')
|
|
366
|
+
|
|
367
|
+
# Materialize NDJSON skips into a JSON array for --slurpfile (stdin only; no
|
|
368
|
+
# growing argv). Empty NDJSON stays the [] written at init.
|
|
369
|
+
if [[ -s "$SKIPPED_NDJSON_FILE" ]]; then
|
|
370
|
+
jq -s '.' "$SKIPPED_NDJSON_FILE" > "$SKIPPED_PATHS_FILE"
|
|
371
|
+
fi
|
|
372
|
+
|
|
373
|
+
jq -n \
|
|
374
|
+
--arg thread_id "$THREAD_ID" \
|
|
375
|
+
--arg thread_path "$THREAD_PATH" \
|
|
376
|
+
--arg ts "$TS" \
|
|
377
|
+
--arg title "$TITLE" \
|
|
378
|
+
--arg summary "$SUMMARY" \
|
|
379
|
+
--arg branch "$BRANCH" \
|
|
380
|
+
--arg started_commit "$START_COMMIT" \
|
|
381
|
+
--arg finalized_commit_note "The exact handoff commit is emitted by handoff-finalize.sh as commit_after_finalize; a file cannot contain the hash of the commit that contains itself." \
|
|
382
|
+
--slurpfile files_touched "$FILES_TOUCHED_FILE" \
|
|
383
|
+
--slurpfile staged_paths "$STAGED_PATHS_FILE" \
|
|
384
|
+
--slurpfile skipped_paths "$SKIPPED_PATHS_FILE" \
|
|
385
|
+
--slurpfile status_summary "$STATUS_SUMMARY_FILE" \
|
|
386
|
+
'{
|
|
387
|
+
thread_id: $thread_id,
|
|
388
|
+
thread_path: $thread_path,
|
|
389
|
+
created_at: $ts,
|
|
390
|
+
intent: { title: $title, summary: $summary },
|
|
391
|
+
git: {
|
|
392
|
+
branch: $branch,
|
|
393
|
+
started_commit: $started_commit,
|
|
394
|
+
finalized_commit: null,
|
|
395
|
+
finalized_commit_note: $finalized_commit_note
|
|
396
|
+
},
|
|
397
|
+
files_touched: ($files_touched[0] // []),
|
|
398
|
+
staged_paths: ($staged_paths[0] // []),
|
|
399
|
+
skipped_paths: ($skipped_paths[0] // []),
|
|
400
|
+
verification: { notes: [], upstream_refs_compared: [] },
|
|
401
|
+
status_summary: ($status_summary[0] // {})
|
|
402
|
+
}' > "$CHANGESET_PATH"
|
|
403
|
+
|
|
404
|
+
# -------- handoff.json pointer --------
|
|
405
|
+
# Keep the pointer readable as one complete file at all times. The temp is in
|
|
406
|
+
# the destination directory so mv publishes it with a same-filesystem rename.
|
|
407
|
+
# The pointer path is checkout-relative so the synced handoff stays portable.
|
|
408
|
+
write_handoff_pointer() {
|
|
409
|
+
local destination="$1" thread_path="$2" destination_dir pointer_tmp
|
|
410
|
+
destination_dir="$(dirname "$destination")"
|
|
411
|
+
mkdir -p "$destination_dir"
|
|
412
|
+
pointer_tmp="$(mktemp "$destination_dir/.handoff-pointer.XXXXXX")"
|
|
413
|
+
FINALIZE_TMP_FILES+=("$pointer_tmp")
|
|
414
|
+
if ! jq -n --arg ts "$TS" --arg msg "$MESSAGE" --arg thread_id "$THREAD_ID" --arg thread_path "$thread_path" --arg summary "$SUMMARY" '{
|
|
415
|
+
created_at: $ts,
|
|
416
|
+
message: $msg,
|
|
417
|
+
last_thread: $thread_id,
|
|
418
|
+
thread_path: $thread_path,
|
|
419
|
+
context_notes: $summary
|
|
420
|
+
}' > "$pointer_tmp"; then
|
|
421
|
+
rm -f "$pointer_tmp"
|
|
422
|
+
return 1
|
|
423
|
+
fi
|
|
424
|
+
if ! mv -f "$pointer_tmp" "$destination"; then
|
|
425
|
+
rm -f "$pointer_tmp"
|
|
426
|
+
return 1
|
|
427
|
+
fi
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
copy_handoff_thread() {
|
|
431
|
+
local destination="$1" destination_dir thread_tmp
|
|
432
|
+
destination_dir="$(dirname "$destination")"
|
|
433
|
+
mkdir -p "$destination_dir"
|
|
434
|
+
thread_tmp="$(mktemp "$destination_dir/.handoff-thread.XXXXXX")"
|
|
435
|
+
FINALIZE_TMP_FILES+=("$thread_tmp")
|
|
436
|
+
if ! cp "$HQ_ROOT/$THREAD_PATH" "$thread_tmp"; then
|
|
437
|
+
rm -f "$thread_tmp"
|
|
438
|
+
return 1
|
|
439
|
+
fi
|
|
440
|
+
if ! mv -f "$thread_tmp" "$destination"; then
|
|
441
|
+
rm -f "$thread_tmp"
|
|
442
|
+
return 1
|
|
443
|
+
fi
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
write_handoff_pointer "workspace/threads/handoff.json" "$THREAD_PATH"
|
|
447
|
+
if [[ "$HAS_GIT_CHECKOUT" == "true" ]]; then
|
|
448
|
+
case "$COMMON_GIT_DIR" in
|
|
449
|
+
/*|[A-Za-z]:/*) ;;
|
|
450
|
+
*) COMMON_GIT_DIR="$HQ_ROOT/$COMMON_GIT_DIR" ;;
|
|
451
|
+
esac
|
|
452
|
+
MAIN_CHECKOUT_ROOT="$(cd "$COMMON_GIT_DIR/.." && pwd -P)"
|
|
453
|
+
if [[ "$MAIN_CHECKOUT_ROOT" != "$HQ_ROOT" ]]; then
|
|
454
|
+
copy_handoff_thread "$MAIN_CHECKOUT_ROOT/$THREAD_PATH"
|
|
455
|
+
write_handoff_pointer "$MAIN_CHECKOUT_ROOT/workspace/threads/handoff.json" "$THREAD_PATH"
|
|
456
|
+
fi
|
|
457
|
+
fi
|
|
458
|
+
|
|
459
|
+
# -------- regen INDEX files via dedicated scripts --------
|
|
460
|
+
# Inline INDEX regen replaced with delegations to reusable bash scripts.
|
|
461
|
+
# Index regen is also callable independently (e.g. /cleanup --reindex,
|
|
462
|
+
# handoff-post.sh). Zero Claude context either way.
|
|
463
|
+
hq core rebuild-index threads --both >/dev/null 2>&1 || true
|
|
464
|
+
hq core rebuild-index orchestrator >/dev/null 2>&1 || true
|
|
465
|
+
|
|
466
|
+
# -------- INDEX.md top-level timestamp only --------
|
|
467
|
+
if [[ -f INDEX.md ]]; then
|
|
468
|
+
# Bump 'Generated:' line in place if present, else skip
|
|
469
|
+
if grep -q '^Generated:' INDEX.md; then
|
|
470
|
+
tmpf=$(mktemp)
|
|
471
|
+
awk -v ts="$TS" '/^Generated:/ { print "Generated: " ts; next } { print }' INDEX.md > "$tmpf"
|
|
472
|
+
mv "$tmpf" INDEX.md
|
|
473
|
+
fi
|
|
474
|
+
fi
|
|
475
|
+
|
|
476
|
+
# -------- HQ commit (explicit paths only — never git add -A) --------
|
|
477
|
+
# A failed commit must never ride inside a success payload. hq_committed alone
|
|
478
|
+
# cannot say why it is false — "there was nothing to commit" and "git refused
|
|
479
|
+
# the commit" are the same value — so hq_commit_status carries the reason and a
|
|
480
|
+
# genuine failure also exits non-zero (4).
|
|
481
|
+
HQ_COMMITTED="false"
|
|
482
|
+
HQ_COMMIT_STATUS="nothing-to-commit"
|
|
483
|
+
if [[ "$HAS_GIT_CHECKOUT" != "true" ]]; then
|
|
484
|
+
HQ_COMMIT_STATUS="no-git-checkout"
|
|
485
|
+
fi
|
|
486
|
+
HQ_COMMIT_ERROR=""
|
|
487
|
+
EXPLICIT_PATHS=(
|
|
488
|
+
"${SAFE_STAGE_PATHS[@]:-}"
|
|
489
|
+
"${MIRROR_STAGE_PATHS[@]:-}"
|
|
490
|
+
"workspace/threads/${THREAD_ID}.json"
|
|
491
|
+
"workspace/threads/${THREAD_ID}.changeset.json"
|
|
492
|
+
"workspace/threads/handoff.json"
|
|
493
|
+
"workspace/threads/recent.md"
|
|
494
|
+
"workspace/threads/INDEX.md"
|
|
495
|
+
"workspace/orchestrator/INDEX.md"
|
|
496
|
+
"INDEX.md"
|
|
497
|
+
)
|
|
498
|
+
STAGED=0
|
|
499
|
+
# Newline-separated, not an array: `${#arr[@]}` on an empty array is an unbound
|
|
500
|
+
# variable under bash 3.2 + `set -u` (the same trap the SAFE_STAGE_PATHS
|
|
501
|
+
# regression hit), and macOS still ships bash 3.2.
|
|
502
|
+
STAGE_FAILURES=""
|
|
503
|
+
STAGE_FAILURE_COUNT=0
|
|
504
|
+
if [[ "$HAS_GIT_CHECKOUT" == "true" ]]; then
|
|
505
|
+
for p in "${EXPLICIT_PATHS[@]}"; do
|
|
506
|
+
[[ -n "$p" ]] || continue
|
|
507
|
+
if [[ -e "$p" || -n "$(git status --porcelain -- "$p" 2>/dev/null)" ]]; then
|
|
508
|
+
add_rc=0
|
|
509
|
+
git add -- "$p" >/dev/null 2>&1 || add_rc=$?
|
|
510
|
+
if [[ $add_rc -eq 0 ]]; then
|
|
511
|
+
STAGED=$((STAGED+1))
|
|
512
|
+
elif git ls-files --error-unmatch -- "$p" >/dev/null 2>&1; then
|
|
513
|
+
# The path is already TRACKED but a broad later ignore rule now shadows it
|
|
514
|
+
# (the scaffold's /workspace/ entry over the tracked handoff pointer/index
|
|
515
|
+
# files), so a plain `git add` refuses it. A tracked file must still be
|
|
516
|
+
# committed, so force-add it. Only a force-add that ALSO fails (e.g. a held
|
|
517
|
+
# .git/index.lock) is a genuine stage failure.
|
|
518
|
+
if git add -f -- "$p" >/dev/null 2>&1; then
|
|
519
|
+
STAGED=$((STAGED+1))
|
|
520
|
+
else
|
|
521
|
+
STAGE_FAILURES="${STAGE_FAILURES}${p}"$'\n'
|
|
522
|
+
STAGE_FAILURE_COUNT=$((STAGE_FAILURE_COUNT+1))
|
|
523
|
+
fi
|
|
524
|
+
elif git check-ignore --no-index -q -- "$p" 2>/dev/null; then
|
|
525
|
+
# An UNtracked, gitignored path is a deliberate exclusion, not a failed
|
|
526
|
+
# write. --no-index is required: plain `git check-ignore` SKIPS tracked
|
|
527
|
+
# files (it would wrongly report a tracked-but-ignored path as not-ignored,
|
|
528
|
+
# the bug that made this a stage-failure); the tracked case is handled above.
|
|
529
|
+
:
|
|
530
|
+
else
|
|
531
|
+
STAGE_FAILURES="${STAGE_FAILURES}${p}"$'\n'
|
|
532
|
+
STAGE_FAILURE_COUNT=$((STAGE_FAILURE_COUNT+1))
|
|
533
|
+
fi
|
|
534
|
+
fi
|
|
535
|
+
done
|
|
536
|
+
fi
|
|
537
|
+
# committed_paths scales with the staged set; capture it to a file (git piped to
|
|
538
|
+
# jq via stdin, then the result written to disk) so it too stays off argv.
|
|
539
|
+
new_tmp COMMITTED_PATHS_FILE
|
|
540
|
+
printf '[]' > "$COMMITTED_PATHS_FILE"
|
|
541
|
+
new_tmp STAGE_FAILURES_FILE
|
|
542
|
+
printf '[]' > "$STAGE_FAILURES_FILE"
|
|
543
|
+
if [[ -n "$STAGE_FAILURES" ]]; then
|
|
544
|
+
printf '%s' "$STAGE_FAILURES" \
|
|
545
|
+
| jq -R -s 'split("\n") | map(select(length > 0))' > "$STAGE_FAILURES_FILE"
|
|
546
|
+
fi
|
|
547
|
+
if [[ $STAGED -gt 0 ]] && [[ -n "$(git diff --cached --name-only 2>/dev/null)" ]]; then
|
|
548
|
+
# Snapshot the staged set separately. It only becomes committed_paths once the
|
|
549
|
+
# commit actually lands; a refused commit must not report paths as committed.
|
|
550
|
+
new_tmp PENDING_PATHS_FILE
|
|
551
|
+
git diff --cached --name-only | jq -R -s 'split("\n") | map(select(length > 0))' > "$PENDING_PATHS_FILE"
|
|
552
|
+
commit_out=""
|
|
553
|
+
commit_rc=0
|
|
554
|
+
commit_out="$(git commit -m "checkpoint: handoff ${THREAD_ID}" 2>&1)" || commit_rc=$?
|
|
555
|
+
if [[ $commit_rc -eq 0 ]]; then
|
|
556
|
+
HQ_COMMITTED="true"
|
|
557
|
+
HQ_COMMIT_STATUS="committed"
|
|
558
|
+
cat "$PENDING_PATHS_FILE" > "$COMMITTED_PATHS_FILE"
|
|
559
|
+
else
|
|
560
|
+
HQ_COMMIT_STATUS="failed"
|
|
561
|
+
HQ_COMMIT_ERROR="$(printf '%s' "$commit_out" | tr '\r\n' ' ' | sed 's/ */ /g; s/^ //; s/ $//' | cut -c1-500)"
|
|
562
|
+
[[ -n "$HQ_COMMIT_ERROR" ]] || HQ_COMMIT_ERROR="git commit exited ${commit_rc} with no output"
|
|
563
|
+
fi
|
|
564
|
+
fi
|
|
565
|
+
# A staging failure is just as fatal as a refused commit, and it is the shape a
|
|
566
|
+
# held .git/index.lock actually takes: every `git add` fails, STAGED stays 0, no
|
|
567
|
+
# commit is even attempted, and the run would otherwise look like the benign
|
|
568
|
+
# "nothing to commit" case while the entire handoff sits uncommitted on disk.
|
|
569
|
+
if [[ $STAGE_FAILURE_COUNT -gt 0 && "$HQ_COMMIT_STATUS" != "failed" ]]; then
|
|
570
|
+
HQ_COMMIT_STATUS="stage-failed"
|
|
571
|
+
if [[ -z "$HQ_COMMIT_ERROR" ]]; then
|
|
572
|
+
HQ_COMMIT_ERROR="git add failed for ${STAGE_FAILURE_COUNT} path(s) — see stage_failures"
|
|
573
|
+
fi
|
|
574
|
+
fi
|
|
575
|
+
COMMIT_AFTER=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
576
|
+
|
|
577
|
+
# -------- qmd reindex fire-and-forget (agent: skip; laptop: single-flight) --------
|
|
578
|
+
# Agent boxes: managed timer owns indexing — handoff never kicks qmd.
|
|
579
|
+
# Laptop: qmd-reindex-bg single-flight reindex. See qmd-reindex-bg.sh.
|
|
580
|
+
# Helper is invoked synchronously: it is fail-soft and either prints
|
|
581
|
+
# skipped-agent / skipped, or launches its own detached worker and prints a PID.
|
|
582
|
+
# qmd_pid must never claim an agent reindex ran.
|
|
583
|
+
QMD_PID=""
|
|
584
|
+
# Prefer Core US-001 helper; fleet binary only if Core file is missing.
|
|
585
|
+
_QMD_BG="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/qmd-reindex-bg.sh"
|
|
586
|
+
if [[ ! -f "$_QMD_BG" ]] && [[ -x /usr/local/bin/hq-agent-qmd-reindex-bg ]]; then
|
|
587
|
+
_QMD_BG=/usr/local/bin/hq-agent-qmd-reindex-bg
|
|
588
|
+
fi
|
|
589
|
+
if [[ -x "$_QMD_BG" ]] || [[ -f "$_QMD_BG" ]]; then
|
|
590
|
+
# bash (not require +x) so a fresh checkout still routes through the helper.
|
|
591
|
+
QMD_PID="$(bash "$_QMD_BG" --log /tmp/qmd-handoff.log 2>/dev/null || true)"
|
|
592
|
+
elif [[ -d /var/lib/hq-agent ]]; then
|
|
593
|
+
# Agent box without helper yet: still do not raw-embed.
|
|
594
|
+
QMD_PID="skipped-agent"
|
|
595
|
+
fi
|
|
596
|
+
|
|
597
|
+
# -------- copy next-step command to clipboard (fail-soft) --------
|
|
598
|
+
[[ "$NEXT_COMMAND" == "__auto__" ]] && NEXT_COMMAND="/resumework ${THREAD_ID}"
|
|
599
|
+
CLIPBOARD_COPIED="false"
|
|
600
|
+
if [[ -n "$NEXT_COMMAND" ]]; then
|
|
601
|
+
if command -v pbcopy >/dev/null 2>&1; then
|
|
602
|
+
printf '%s' "$NEXT_COMMAND" | pbcopy 2>/dev/null && CLIPBOARD_COPIED="true"
|
|
603
|
+
elif command -v wl-copy >/dev/null 2>&1; then
|
|
604
|
+
printf '%s' "$NEXT_COMMAND" | wl-copy 2>/dev/null && CLIPBOARD_COPIED="true"
|
|
605
|
+
elif command -v xclip >/dev/null 2>&1; then
|
|
606
|
+
printf '%s' "$NEXT_COMMAND" | xclip -selection clipboard 2>/dev/null && CLIPBOARD_COPIED="true"
|
|
607
|
+
fi
|
|
608
|
+
fi
|
|
609
|
+
|
|
610
|
+
# -------- emit result --------
|
|
611
|
+
jq -n \
|
|
612
|
+
--arg thread_id "$THREAD_ID" \
|
|
613
|
+
--arg thread_path "$THREAD_PATH" \
|
|
614
|
+
--arg handoff_path "workspace/threads/handoff.json" \
|
|
615
|
+
--argjson hq_committed "$HQ_COMMITTED" \
|
|
616
|
+
--argjson indexes_regen "true" \
|
|
617
|
+
--arg qmd_pid "${QMD_PID:-}" \
|
|
618
|
+
--arg git_bg_errors "$GIT_BG_ERRORS" \
|
|
619
|
+
--arg changeset_path "$CHANGESET_PATH" \
|
|
620
|
+
--arg hq_commit_status "$HQ_COMMIT_STATUS" \
|
|
621
|
+
--arg hq_commit_error "$HQ_COMMIT_ERROR" \
|
|
622
|
+
--slurpfile committed_paths "$COMMITTED_PATHS_FILE" \
|
|
623
|
+
--slurpfile stage_failures "$STAGE_FAILURES_FILE" \
|
|
624
|
+
--slurpfile skipped_paths "$SKIPPED_PATHS_FILE" \
|
|
625
|
+
--argjson baseline_noise_count "$BASELINE_NOISE_COUNT" \
|
|
626
|
+
--arg commit_after_finalize "$COMMIT_AFTER" \
|
|
627
|
+
--arg next_command "$NEXT_COMMAND" \
|
|
628
|
+
--argjson clipboard_copied "$CLIPBOARD_COPIED" \
|
|
629
|
+
--argjson mirror_companies "$MIRRORED_COMPANIES_JSON" \
|
|
630
|
+
'{
|
|
631
|
+
thread_id: $thread_id,
|
|
632
|
+
thread_path: $thread_path,
|
|
633
|
+
changeset_path: $changeset_path,
|
|
634
|
+
handoff_path: $handoff_path,
|
|
635
|
+
hq_committed: $hq_committed,
|
|
636
|
+
hq_commit_status: $hq_commit_status,
|
|
637
|
+
hq_commit_error: $hq_commit_error,
|
|
638
|
+
commit_after_finalize: $commit_after_finalize,
|
|
639
|
+
committed_paths: ($committed_paths[0] // []),
|
|
640
|
+
stage_failures: ($stage_failures[0] // []),
|
|
641
|
+
skipped_paths: ($skipped_paths[0] // []),
|
|
642
|
+
baseline_noise_count: $baseline_noise_count,
|
|
643
|
+
indexes_regen: $indexes_regen,
|
|
644
|
+
qmd_pid: $qmd_pid,
|
|
645
|
+
git_bg_errors: $git_bg_errors,
|
|
646
|
+
next_command: $next_command,
|
|
647
|
+
clipboard_copied: $clipboard_copied,
|
|
648
|
+
mirror_companies: $mirror_companies
|
|
649
|
+
}'
|
|
650
|
+
|
|
651
|
+
# -------- fail loudly on a git write that did not land --------
|
|
652
|
+
# The payload above is always emitted first: the caller still needs the thread
|
|
653
|
+
# id and paths to recover. What changes is that a refused commit no longer
|
|
654
|
+
# leaves the process looking successful.
|
|
655
|
+
if [[ -n "$GIT_BG_ERRORS" ]]; then
|
|
656
|
+
echo "handoff-finalize: WARNING background git step reported errors: $GIT_BG_ERRORS" >&2
|
|
657
|
+
fi
|
|
658
|
+
if [[ $STAGE_FAILURE_COUNT -gt 0 ]]; then
|
|
659
|
+
echo "handoff-finalize: ERROR git add failed for ${STAGE_FAILURE_COUNT} path(s):" >&2
|
|
660
|
+
printf '%s' "$STAGE_FAILURES" | sed 's/^/handoff-finalize: /' >&2
|
|
661
|
+
fi
|
|
662
|
+
case "$HQ_COMMIT_STATUS" in
|
|
663
|
+
failed|stage-failed)
|
|
664
|
+
echo "handoff-finalize: ERROR the HQ handoff commit FAILED — thread files were written but are NOT committed." >&2
|
|
665
|
+
echo "handoff-finalize: git said: $HQ_COMMIT_ERROR" >&2
|
|
666
|
+
echo "handoff-finalize: resolve the git error and re-run /handoff before ending the session." >&2
|
|
667
|
+
exit 4
|
|
668
|
+
;;
|
|
669
|
+
esac
|
|
670
|
+
exit 0
|