@link-assistant/hive-mind 1.55.0 → 1.56.1
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 +31 -0
- package/README.md +21 -111
- package/package.json +4 -3
- package/src/github.lib.mjs +14 -2
- package/src/hive-screens.lib.mjs +287 -0
- package/src/hive-screens.mjs +18 -0
- package/src/solve.pre-pr-failure-notifier.lib.mjs +26 -12
- package/src/solve.repository.lib.mjs +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.56.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 32035a2: Issue #1651: When fork-parent auto-recovery tries to delete the mismatched
|
|
8
|
+
fork and the GitHub CLI token is missing the `delete_repo` scope, `solve`
|
|
9
|
+
now prints the real remediation (`gh auth refresh -h github.com -s delete_repo`)
|
|
10
|
+
plus a non-destructive alternative (rename/archive + `--prefix-fork-name-with-owner-name`)
|
|
11
|
+
instead of re-recommending the same `gh repo delete` command that just failed.
|
|
12
|
+
In `--verbose` mode the full `gh` output is also printed so future root-cause
|
|
13
|
+
analyses have the diagnostic lines GitHub already provides.
|
|
14
|
+
|
|
15
|
+
Pre-PR failures that are posted back to GitHub issues now use user-facing
|
|
16
|
+
guidance: they ask the issue reporter to fix repository/account state when
|
|
17
|
+
possible or ask a Hive Mind administrator to handle the affected repository,
|
|
18
|
+
while keeping administrator CLI details in the terminal log instead of the
|
|
19
|
+
public issue comment.
|
|
20
|
+
|
|
21
|
+
## 1.56.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- 391dbde: Add `hive-screens` bin command. Converts the `hive-screens.sh` script that was
|
|
26
|
+
embedded in README.md into a real JavaScript command shipped with the package.
|
|
27
|
+
Supports `--list` (safe preview), `--enter` (attach), and `--close` (terminate)
|
|
28
|
+
across detached GNU screen sessions that completed a mergeable solve run.
|
|
29
|
+
`--list`, `--enter`, and `--close` share the same matching predicate, so any
|
|
30
|
+
session visible under `--list` is guaranteed to be actionable by the other
|
|
31
|
+
flags. Selection flags `--oldest` (default), `--newest`, and `--all` are
|
|
32
|
+
preserved from the legacy script. Closes #1649.
|
|
33
|
+
|
|
3
34
|
## 1.55.0
|
|
4
35
|
|
|
5
36
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -818,124 +818,34 @@ s=$(screen -ls | awk '/Detached/ {last=$1} END{print last}'); echo "Entering $s"
|
|
|
818
818
|
|
|
819
819
|
### Script for managing screens
|
|
820
820
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
enter=false
|
|
826
|
-
close=false
|
|
827
|
-
oldest=false
|
|
828
|
-
newest=false
|
|
829
|
-
all=false
|
|
830
|
-
|
|
831
|
-
# --- parse args ---
|
|
832
|
-
for arg in "$@"; do
|
|
833
|
-
case "$arg" in
|
|
834
|
-
--enter) enter=true ;;
|
|
835
|
-
--close) close=true ;;
|
|
836
|
-
--oldest) oldest=true ;;
|
|
837
|
-
--newest) newest=true ;;
|
|
838
|
-
--all) all=true ;;
|
|
839
|
-
*)
|
|
840
|
-
echo "Unknown option: $arg"
|
|
841
|
-
exit 1
|
|
842
|
-
;;
|
|
843
|
-
esac
|
|
844
|
-
done
|
|
845
|
-
|
|
846
|
-
# --- validate ---
|
|
847
|
-
if ! $enter && ! $close; then
|
|
848
|
-
echo "Must specify --enter or --close"
|
|
849
|
-
exit 1
|
|
850
|
-
fi
|
|
851
|
-
|
|
852
|
-
# --- default ---
|
|
853
|
-
if ! $oldest && ! $newest && ! $all; then
|
|
854
|
-
oldest=true
|
|
855
|
-
fi
|
|
856
|
-
|
|
857
|
-
matches=()
|
|
858
|
-
|
|
859
|
-
# --- sorting ---
|
|
860
|
-
if $newest; then
|
|
861
|
-
sorter="sort -nr"
|
|
862
|
-
else
|
|
863
|
-
sorter="sort -n"
|
|
864
|
-
fi
|
|
865
|
-
|
|
866
|
-
# --- scan sessions ---
|
|
867
|
-
while read -r sess; do
|
|
868
|
-
tmp=$(mktemp)
|
|
869
|
-
clean=$(mktemp)
|
|
870
|
-
|
|
871
|
-
# FIX: better capture
|
|
872
|
-
screen -S "$sess" -X scrollback 200000 2>/dev/null
|
|
873
|
-
sleep 0.15
|
|
874
|
-
screen -S "$sess" -X hardcopy -h "$tmp" 2>/dev/null
|
|
875
|
-
|
|
876
|
-
# strip garbage / non-printable chars
|
|
877
|
-
tr -cd '\11\12\15\40-\176' < "$tmp" > "$clean"
|
|
878
|
-
|
|
879
|
-
if grep -qi 'process completed' "$clean" &&
|
|
880
|
-
grep -qiE 'pr is mergeable!|pr merged!' "$clean"; then
|
|
881
|
-
|
|
882
|
-
log_path=$(tac "$clean" | grep -m1 -i 'full log file:' \
|
|
883
|
-
| sed 's/.*Full log file:[[:space:]]*//')
|
|
884
|
-
|
|
885
|
-
issue=$(tac "$clean" | grep -m1 -i 'Issue:[[:space:]]*https://github\.com/' \
|
|
886
|
-
| sed 's/Issue:[[:space:]]*//')
|
|
887
|
-
|
|
888
|
-
matches+=("$sess|$log_path|$issue")
|
|
889
|
-
fi
|
|
890
|
-
|
|
891
|
-
rm -f "$tmp" "$clean"
|
|
892
|
-
done < <(screen -ls | awk '/Detached/ {print $1}' | $sorter)
|
|
893
|
-
|
|
894
|
-
# --- no matches ---
|
|
895
|
-
if [ ${#matches[@]} -eq 0 ]; then
|
|
896
|
-
echo "No matching sessions"
|
|
897
|
-
exit 0
|
|
898
|
-
fi
|
|
899
|
-
|
|
900
|
-
process_one() {
|
|
901
|
-
IFS="|" read -r sess log issue <<< "$1"
|
|
902
|
-
|
|
903
|
-
echo "Session: $sess"
|
|
904
|
-
|
|
905
|
-
if $enter; then
|
|
906
|
-
echo "Entering $sess"
|
|
907
|
-
screen -r "$sess"
|
|
908
|
-
echo "Left $sess"
|
|
909
|
-
fi
|
|
821
|
+
The legacy `hive-screens.sh` script has been promoted to a first-class command:
|
|
822
|
+
`hive-screens`. It ships with `@link-assistant/hive-mind`, so once the package is
|
|
823
|
+
installed (globally, through `npx`, or in a project) it is available on `PATH`.
|
|
910
824
|
|
|
911
|
-
|
|
912
|
-
|
|
825
|
+
It scans detached GNU screen sessions, looks for solve runs that are done and
|
|
826
|
+
mergeable (scrollback contains both `process completed` and `PR is mergeable!`
|
|
827
|
+
or `PR merged!`), and then either lists, enters, or closes them. `--list`,
|
|
828
|
+
`--enter`, and `--close` share the **same matching predicate**, so anything
|
|
829
|
+
you see under `--list` is guaranteed to be the same set `--close` will act on
|
|
830
|
+
— use `--list` first to debug, then rerun with `--close`.
|
|
913
831
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
fi
|
|
918
|
-
|
|
919
|
-
echo "-----------------------------------"
|
|
920
|
-
}
|
|
832
|
+
```bash
|
|
833
|
+
# Safe preview — show every finished, mergeable solve session.
|
|
834
|
+
hive-screens --list --all
|
|
921
835
|
|
|
922
|
-
#
|
|
923
|
-
|
|
924
|
-
for m in "${matches[@]}"; do
|
|
925
|
-
process_one "$m"
|
|
926
|
-
done
|
|
927
|
-
elif $oldest; then
|
|
928
|
-
process_one "${matches[0]}"
|
|
929
|
-
elif $newest; then
|
|
930
|
-
last_index=$((${#matches[@]} - 1))
|
|
931
|
-
process_one "${matches[$last_index]}"
|
|
932
|
-
fi
|
|
836
|
+
# Close the oldest finished session (same as the legacy script's default).
|
|
837
|
+
hive-screens --close
|
|
933
838
|
|
|
934
|
-
|
|
839
|
+
# Attach to the newest finished session.
|
|
840
|
+
hive-screens --enter --newest
|
|
935
841
|
|
|
936
|
-
|
|
842
|
+
# Close every finished session.
|
|
843
|
+
hive-screens --close --all
|
|
937
844
|
```
|
|
938
845
|
|
|
846
|
+
Selection defaults to `--oldest`. Supply `--newest` or `--all` to change it.
|
|
847
|
+
Run `hive-screens --help` for the full option list.
|
|
848
|
+
|
|
939
849
|
### Reboot server.
|
|
940
850
|
|
|
941
851
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/hive-mind",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.56.1",
|
|
4
4
|
"description": "AI-powered issue solver and hive mind for collaborative problem solving",
|
|
5
5
|
"main": "src/hive.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
"review": "./src/review.mjs",
|
|
12
12
|
"configure-claude": "./src/configure-claude.mjs",
|
|
13
13
|
"start-screen": "./src/start-screen.mjs",
|
|
14
|
+
"hive-screens": "./src/hive-screens.mjs",
|
|
14
15
|
"hive-telegram-bot": "./src/telegram-bot.mjs"
|
|
15
16
|
},
|
|
16
17
|
"scripts": {
|
|
17
|
-
"test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-codex-support.mjs && node tests/test-build-cost-info-string.mjs && node tests/test-claude-code-install-method.mjs && node tests/test-claude-quiet-config.mjs && node tests/test-configure-claude-bin.mjs && node tests/test-docker-release-order.mjs && node tests/test-docker-box-migration.mjs && node tests/test-issue-1616-pr-issue-link-preservation.mjs && node tests/test-pre-pr-failure-notifier-1640.mjs && node tests/test-ready-to-merge-pagination-1645.mjs && node tests/test-require-gh-paginate-rule.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-telegram-bot-command-aliases.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-telegram-bot-launcher.mjs",
|
|
18
|
+
"test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-codex-support.mjs && node tests/test-build-cost-info-string.mjs && node tests/test-claude-code-install-method.mjs && node tests/test-claude-quiet-config.mjs && node tests/test-configure-claude-bin.mjs && node tests/test-docker-release-order.mjs && node tests/test-docker-box-migration.mjs && node tests/test-hive-screens.mjs && node tests/test-issue-1616-pr-issue-link-preservation.mjs && node tests/test-pre-pr-failure-notifier-1640.mjs && node tests/test-ready-to-merge-pagination-1645.mjs && node tests/test-require-gh-paginate-rule.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-telegram-bot-command-aliases.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-telegram-bot-launcher.mjs",
|
|
18
19
|
"test:queue": "node tests/solve-queue.test.mjs",
|
|
19
20
|
"test:limits-display": "node tests/limits-display.test.mjs",
|
|
20
21
|
"test:usage-limit": "node tests/test-usage-limit.mjs",
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
"changeset": "changeset",
|
|
27
28
|
"changeset:version": "changeset version",
|
|
28
29
|
"changeset:publish": "npm run build:pre && changeset publish",
|
|
29
|
-
"build:pre": "chmod +x src/hive.mjs && chmod +x src/solve.mjs && chmod +x src/configure-claude.mjs",
|
|
30
|
+
"build:pre": "chmod +x src/hive.mjs && chmod +x src/solve.mjs && chmod +x src/configure-claude.mjs && chmod +x src/hive-screens.mjs",
|
|
30
31
|
"prepare": "husky"
|
|
31
32
|
},
|
|
32
33
|
"repository": {
|
package/src/github.lib.mjs
CHANGED
|
@@ -21,6 +21,18 @@ export { buildCostInfoString };
|
|
|
21
21
|
import { SOLUTION_DRAFT_LOG_MARKER, SOLUTION_DRAFT_FAILED_MARKER, SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER, USAGE_LIMIT_REACHED_MARKER, NOW_WORKING_SESSION_IS_ENDED_MARKER, postTrackedComment, postTrackedCommentFromFile } from './tool-comments.lib.mjs';
|
|
22
22
|
export const maskGitHubToken = maskToken; // Alias for backward compatibility
|
|
23
23
|
export const escapeCodeBlocksInLog = logContent => logContent.replace(/```/g, '\\`\\`\\`'); // Escape ``` in logs
|
|
24
|
+
const buildIssueFailureActionSection = targetType => {
|
|
25
|
+
if (targetType !== 'issue') return '';
|
|
26
|
+
|
|
27
|
+
return `
|
|
28
|
+
|
|
29
|
+
### What you can do
|
|
30
|
+
- Resolve the repository, account, permissions, or environment problem described above, then rerun the solver.
|
|
31
|
+
- If this requires elevated Hive Mind access, ask a Hive Mind administrator to handle the specific failure described above.
|
|
32
|
+
- Repository deletion can require a separate GitHub account or token with repository deletion permission; Hive Mind does not rely on that permission by default.
|
|
33
|
+
|
|
34
|
+
Administrator-only CLI details, if any, are printed in the solver terminal log rather than in this issue comment.`;
|
|
35
|
+
};
|
|
24
36
|
export const checkFileInBranch = async (owner, repo, fileName, branchName) => {
|
|
25
37
|
const { $ } = await use('command-stream');
|
|
26
38
|
|
|
@@ -486,7 +498,7 @@ ${footerNote}`;
|
|
|
486
498
|
The automated solution draft encountered an error:
|
|
487
499
|
\`\`\`
|
|
488
500
|
${errorMessage}
|
|
489
|
-
\`\`\`${modelInfoString}
|
|
501
|
+
\`\`\`${buildIssueFailureActionSection(targetType)}${modelInfoString}
|
|
490
502
|
|
|
491
503
|
<details>
|
|
492
504
|
<summary>Click to expand failure log (${Math.round(logStats.size / 1024)}KB)</summary>
|
|
@@ -675,7 +687,7 @@ ${uploadFooterNote}`;
|
|
|
675
687
|
The automated solution draft encountered an error:
|
|
676
688
|
\`\`\`
|
|
677
689
|
${errorMessage}
|
|
678
|
-
\`\`\`${modelInfoString}
|
|
690
|
+
\`\`\`${buildIssueFailureActionSection(targetType)}${modelInfoString}
|
|
679
691
|
|
|
680
692
|
### 📎 **Failure log uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
|
|
681
693
|
- [View complete failure log](${logUrl})
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared runner for the `hive-screens` bin command. Scans detached GNU
|
|
5
|
+
* screen sessions for completed solve runs and lists, enters, or closes
|
|
6
|
+
* them. Ports the `hive-screens.sh` script that previously lived in
|
|
7
|
+
* README.md, and keeps a single matching function so that `--list` is
|
|
8
|
+
* a safe preview for `--close` / `--enter`.
|
|
9
|
+
*
|
|
10
|
+
* See issue #1649.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { exec as execCallback } from 'node:child_process';
|
|
14
|
+
import fs from 'node:fs/promises';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { promisify } from 'node:util';
|
|
18
|
+
|
|
19
|
+
const execAsync = promisify(execCallback);
|
|
20
|
+
|
|
21
|
+
export const HIVE_SCREENS_HELP = `Usage: hive-screens (--list | --enter | --close) [--oldest|--newest|--all]
|
|
22
|
+
|
|
23
|
+
Scan detached GNU screen sessions for completed solve runs and either list,
|
|
24
|
+
enter, or close them. A session matches when its scrollback contains both
|
|
25
|
+
"process completed" and either "pr is mergeable!" or "pr merged!" (case
|
|
26
|
+
insensitive) — the exact predicate from the legacy hive-screens.sh script.
|
|
27
|
+
|
|
28
|
+
Actions (one required):
|
|
29
|
+
--list Print matching sessions without touching them
|
|
30
|
+
--enter Attach to the selected match (blocking)
|
|
31
|
+
--close Send \`exit\\n\` to the selected match so it terminates
|
|
32
|
+
|
|
33
|
+
Selection (optional, default: --oldest):
|
|
34
|
+
--oldest Act on the oldest match (default)
|
|
35
|
+
--newest Act on the newest match
|
|
36
|
+
--all Act on every match in oldest-first order
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
-h, --help Show this help and exit
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
hive-screens --list # safe preview of matches
|
|
43
|
+
hive-screens --list --all # preview every match
|
|
44
|
+
hive-screens --close --oldest # close the oldest finished run
|
|
45
|
+
hive-screens --enter --newest # attach to the newest finished run
|
|
46
|
+
|
|
47
|
+
Reference: https://github.com/link-assistant/hive-mind/issues/1649
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const ACTION_FLAGS = new Set(['--enter', '--close', '--list']);
|
|
51
|
+
const SELECTION_FLAGS = new Set(['--oldest', '--newest', '--all']);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse the argv for `hive-screens`. Returns the parsed flags plus an
|
|
55
|
+
* `error` string when validation fails (so callers can print it and exit
|
|
56
|
+
* with a non-zero status without throwing).
|
|
57
|
+
*/
|
|
58
|
+
export const parseHiveScreensArgs = argv => {
|
|
59
|
+
const result = {
|
|
60
|
+
enter: false,
|
|
61
|
+
close: false,
|
|
62
|
+
list: false,
|
|
63
|
+
selection: null,
|
|
64
|
+
help: false,
|
|
65
|
+
error: null,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
for (const arg of argv) {
|
|
69
|
+
if (arg === '--help' || arg === '-h') {
|
|
70
|
+
result.help = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (ACTION_FLAGS.has(arg)) {
|
|
74
|
+
if (arg === '--enter') result.enter = true;
|
|
75
|
+
else if (arg === '--close') result.close = true;
|
|
76
|
+
else result.list = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (SELECTION_FLAGS.has(arg)) {
|
|
80
|
+
if (result.selection && result.selection !== arg.slice(2)) {
|
|
81
|
+
result.error = `Conflicting selection flags: --${result.selection} and ${arg}`;
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
result.selection = arg.slice(2);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
result.error = `Unknown option: ${arg}`;
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (result.help) return result;
|
|
92
|
+
|
|
93
|
+
const actions = [result.enter, result.close, result.list].filter(Boolean).length;
|
|
94
|
+
if (actions === 0) {
|
|
95
|
+
result.error = 'Must specify --list, --enter, or --close';
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
if (actions > 1) {
|
|
99
|
+
result.error = 'Specify only one of --list, --enter, --close';
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!result.selection) result.selection = 'oldest';
|
|
104
|
+
return result;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* List detached screen sessions in oldest-first order. GNU screen prints
|
|
109
|
+
* them newest-first, so we reverse to mirror `sort -n` (ascending PID) on
|
|
110
|
+
* the typical `NNNNN.name` session names.
|
|
111
|
+
*/
|
|
112
|
+
export const listDetachedSessions = async ({ exec = execAsync } = {}) => {
|
|
113
|
+
let stdout = '';
|
|
114
|
+
try {
|
|
115
|
+
({ stdout } = await exec('screen -ls'));
|
|
116
|
+
} catch (err) {
|
|
117
|
+
// `screen -ls` exits 1 when there are no sessions. It still prints the
|
|
118
|
+
// session header to stdout, so keep parsing whatever we got.
|
|
119
|
+
stdout = err.stdout || '';
|
|
120
|
+
}
|
|
121
|
+
const sessions = [];
|
|
122
|
+
for (const rawLine of stdout.split('\n')) {
|
|
123
|
+
const line = rawLine.trim();
|
|
124
|
+
if (!/\((?:Detached|Attached)\)/i.test(line)) continue;
|
|
125
|
+
if (!/Detached/i.test(line)) continue;
|
|
126
|
+
const match = line.match(/^(\S+)/);
|
|
127
|
+
if (match) sessions.push(match[1]);
|
|
128
|
+
}
|
|
129
|
+
return sessions.sort((a, b) => {
|
|
130
|
+
const na = parseInt(a, 10);
|
|
131
|
+
const nb = parseInt(b, 10);
|
|
132
|
+
if (Number.isNaN(na) || Number.isNaN(nb)) return a.localeCompare(b);
|
|
133
|
+
return na - nb;
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
138
|
+
|
|
139
|
+
const stripNonPrintable = text => text.replace(/[^\t\n\r\x20-\x7E]/g, '');
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Capture the scrollback of a single screen session. Mirrors the sh
|
|
143
|
+
* script: bump scrollback to 200000, settle, `hardcopy -h`, read, strip.
|
|
144
|
+
*/
|
|
145
|
+
export const captureSessionScrollback = async (session, { exec = execAsync, fsModule = fs, tmpDir = os.tmpdir(), scrollback = 200000, settleMs = 150 } = {}) => {
|
|
146
|
+
const tmpFile = path.join(tmpDir, `hive-screens-${session}-${Date.now()}-${Math.random().toString(36).slice(2)}.hardcopy`);
|
|
147
|
+
const shellSession = session.replace(/'/g, "'\\''");
|
|
148
|
+
const shellTmp = tmpFile.replace(/'/g, "'\\''");
|
|
149
|
+
try {
|
|
150
|
+
await exec(`screen -S '${shellSession}' -X scrollback ${scrollback}`).catch(() => {});
|
|
151
|
+
if (settleMs > 0) await sleep(settleMs);
|
|
152
|
+
await exec(`screen -S '${shellSession}' -X hardcopy -h '${shellTmp}'`).catch(() => {});
|
|
153
|
+
let raw = '';
|
|
154
|
+
try {
|
|
155
|
+
raw = await fsModule.readFile(tmpFile, 'utf-8');
|
|
156
|
+
} catch {
|
|
157
|
+
raw = '';
|
|
158
|
+
}
|
|
159
|
+
return stripNonPrintable(raw);
|
|
160
|
+
} finally {
|
|
161
|
+
await fsModule.unlink(tmpFile).catch(() => {});
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The single source of truth for session matching. `--list`, `--enter`,
|
|
167
|
+
* and `--close` all route through this predicate, so a session visible in
|
|
168
|
+
* `--list` is guaranteed to be actionable by the other two flags.
|
|
169
|
+
*/
|
|
170
|
+
export const sessionMatches = text => {
|
|
171
|
+
if (!text) return { matched: false, logPath: null, issueUrl: null };
|
|
172
|
+
const hasCompletion = /process completed/i.test(text);
|
|
173
|
+
const hasMerge = /pr is mergeable!|pr merged!/i.test(text);
|
|
174
|
+
if (!hasCompletion || !hasMerge) {
|
|
175
|
+
return { matched: false, logPath: null, issueUrl: null };
|
|
176
|
+
}
|
|
177
|
+
const logMatches = [...text.matchAll(/Full log file:\s*(\S+)/gi)];
|
|
178
|
+
const issueMatches = [...text.matchAll(/Issue:\s*(https:\/\/github\.com\/\S+)/gi)];
|
|
179
|
+
return {
|
|
180
|
+
matched: true,
|
|
181
|
+
logPath: logMatches.length ? logMatches[logMatches.length - 1][1] : null,
|
|
182
|
+
issueUrl: issueMatches.length ? issueMatches[issueMatches.length - 1][1] : null,
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Scan every detached session and return the ones that pass
|
|
188
|
+
* `sessionMatches`, in the requested order.
|
|
189
|
+
*/
|
|
190
|
+
export const findMatchingSessions = async ({ exec = execAsync, fsModule = fs, tmpDir = os.tmpdir(), order = 'oldest', captureOptions = {} } = {}) => {
|
|
191
|
+
const sessions = await listDetachedSessions({ exec });
|
|
192
|
+
const ordered = order === 'newest' ? [...sessions].reverse() : sessions;
|
|
193
|
+
const matches = [];
|
|
194
|
+
for (const session of ordered) {
|
|
195
|
+
const text = await captureSessionScrollback(session, { exec, fsModule, tmpDir, ...captureOptions });
|
|
196
|
+
const result = sessionMatches(text);
|
|
197
|
+
if (result.matched) {
|
|
198
|
+
matches.push({ session, logPath: result.logPath, issueUrl: result.issueUrl });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return matches;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Apply `--oldest / --newest / --all` to the ordered match list produced
|
|
206
|
+
* by `findMatchingSessions`. The orderer already did the directional
|
|
207
|
+
* sort, so picking element 0 is always "the selected one" in that order.
|
|
208
|
+
*/
|
|
209
|
+
export const selectMatches = (matches, selection) => {
|
|
210
|
+
if (!matches.length) return [];
|
|
211
|
+
if (selection === 'all') return matches;
|
|
212
|
+
return [matches[0]];
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const printSession = ({ session, logPath, issueUrl }, { log }) => {
|
|
216
|
+
log(`Session: ${session}`);
|
|
217
|
+
log(logPath ? `Log: ${logPath}` : 'Log: (not found)');
|
|
218
|
+
log(issueUrl ? `Issue: ${issueUrl}` : 'Issue: (not found)');
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const SEPARATOR = '-----------------------------------';
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Top-level orchestrator used by the bin. `deps` is injected so tests can
|
|
225
|
+
* stub `exec`, `fs`, stdio, and process spawning without touching real
|
|
226
|
+
* screen sessions.
|
|
227
|
+
*/
|
|
228
|
+
export const runHiveScreens = async (argv, deps = {}) => {
|
|
229
|
+
const { exec = execAsync, fsModule = fs, tmpDir = os.tmpdir(), log = (...args) => console.log(...args), error = (...args) => console.error(...args), spawnScreen, captureOptions } = deps;
|
|
230
|
+
|
|
231
|
+
const args = parseHiveScreensArgs(argv);
|
|
232
|
+
if (args.help) {
|
|
233
|
+
log(HIVE_SCREENS_HELP);
|
|
234
|
+
return 0;
|
|
235
|
+
}
|
|
236
|
+
if (args.error) {
|
|
237
|
+
error(args.error);
|
|
238
|
+
return 1;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const order = args.selection === 'newest' ? 'newest' : 'oldest';
|
|
242
|
+
const matches = await findMatchingSessions({ exec, fsModule, tmpDir, order, captureOptions });
|
|
243
|
+
|
|
244
|
+
if (!matches.length) {
|
|
245
|
+
log('No matching sessions');
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const selected = selectMatches(matches, args.selection);
|
|
250
|
+
|
|
251
|
+
for (const match of selected) {
|
|
252
|
+
printSession(match, { log });
|
|
253
|
+
if (args.enter) {
|
|
254
|
+
log(`Entering ${match.session}`);
|
|
255
|
+
if (spawnScreen) {
|
|
256
|
+
await spawnScreen(match.session);
|
|
257
|
+
} else {
|
|
258
|
+
await attachScreen(match.session);
|
|
259
|
+
}
|
|
260
|
+
log(`Left ${match.session}`);
|
|
261
|
+
}
|
|
262
|
+
if (args.close) {
|
|
263
|
+
log(`Closing ${match.session}`);
|
|
264
|
+
const shellSession = match.session.replace(/'/g, "'\\''");
|
|
265
|
+
await exec(`screen -S '${shellSession}' -X stuff $'exit\\n'`).catch(err => {
|
|
266
|
+
error(`Failed to send exit to ${match.session}: ${err.message}`);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
log(SEPARATOR);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return 0;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Default `--enter` side-effect: spawn `screen -r <session>` attached to
|
|
277
|
+
* the parent stdio so the user can actually interact with it. Split from
|
|
278
|
+
* `runHiveScreens` so tests can inject a no-op spawn.
|
|
279
|
+
*/
|
|
280
|
+
const attachScreen = async session => {
|
|
281
|
+
const { spawn } = await import('node:child_process');
|
|
282
|
+
return new Promise((resolve, reject) => {
|
|
283
|
+
const child = spawn('screen', ['-r', session], { stdio: 'inherit' });
|
|
284
|
+
child.on('error', reject);
|
|
285
|
+
child.on('exit', () => resolve());
|
|
286
|
+
});
|
|
287
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `hive-screens` — list, enter, or close detached GNU screen sessions
|
|
5
|
+
* produced by `solve` / `hive` runs that have completed a mergeable PR.
|
|
6
|
+
*
|
|
7
|
+
* Replaces the embedded `hive-screens.sh` script that previously lived
|
|
8
|
+
* in README.md. The matching predicate is shared across `--list`,
|
|
9
|
+
* `--enter`, and `--close`, so any session visible under `--list` is
|
|
10
|
+
* guaranteed to be actionable by the other two flags.
|
|
11
|
+
*
|
|
12
|
+
* See issue #1649.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { runHiveScreens } from './hive-screens.lib.mjs';
|
|
16
|
+
|
|
17
|
+
const exitCode = await runHiveScreens(process.argv.slice(2));
|
|
18
|
+
process.exit(exitCode);
|
|
@@ -8,6 +8,26 @@ const truncate = (value, maxLength = 2000) => {
|
|
|
8
8
|
|
|
9
9
|
const fence = value => truncate(value || 'Unknown error').replaceAll('```', '` ` `');
|
|
10
10
|
|
|
11
|
+
export function buildPrePullRequestFailureActionSection(reason = '') {
|
|
12
|
+
const normalizedReason = String(reason || '').toLowerCase();
|
|
13
|
+
const isForkOrRecoveryFailure = normalizedReason.includes('fork') || normalizedReason.includes('auto-recovery') || normalizedReason.includes('repository setup');
|
|
14
|
+
|
|
15
|
+
if (isForkOrRecoveryFailure) {
|
|
16
|
+
return `### What you can do
|
|
17
|
+
- If the affected fork or repository belongs to you, remove, rename, archive, initialize, or otherwise repair it in GitHub, then rerun the solver.
|
|
18
|
+
- If the action requires elevated Hive Mind access, ask a Hive Mind administrator to handle the affected fork or repository and rerun the solver.
|
|
19
|
+
- Repository deletion can require a separate GitHub account or token with repository deletion permission; Hive Mind does not rely on that permission by default.
|
|
20
|
+
|
|
21
|
+
Administrator-only CLI details, if any, are printed in the solver terminal log rather than in this issue comment.`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return `### What you can do
|
|
25
|
+
- Resolve the repository, account, permissions, or environment problem described above, then rerun the solver.
|
|
26
|
+
- If this requires elevated Hive Mind access, ask a Hive Mind administrator to handle the specific failure described above.
|
|
27
|
+
|
|
28
|
+
Administrator-only CLI details, if any, are printed in the solver terminal log rather than in this issue comment.`;
|
|
29
|
+
}
|
|
30
|
+
|
|
11
31
|
export function shouldNotifyIssueAboutPrePullRequestFailure({ code, globalState }) {
|
|
12
32
|
if (code === 0) return false;
|
|
13
33
|
if (!globalState?.issueNumber || !globalState?.owner || !globalState?.repo) return false;
|
|
@@ -16,18 +36,11 @@ export function shouldNotifyIssueAboutPrePullRequestFailure({ code, globalState
|
|
|
16
36
|
return getTrackedToolCommentIds().size === 0;
|
|
17
37
|
}
|
|
18
38
|
|
|
19
|
-
export function buildPrePullRequestFailureComment({ reason, owner, repo, issueNumber, argv = {},
|
|
39
|
+
export function buildPrePullRequestFailureComment({ reason, owner, repo, issueNumber, argv = {}, logAttachmentAttempted = false }) {
|
|
20
40
|
const tool = argv.tool || 'claude';
|
|
21
41
|
const modelLine = argv.model ? `\n- **Requested model**: \`${argv.model}\`` : '';
|
|
22
|
-
const commandBlock = rawCommand
|
|
23
|
-
? `
|
|
24
|
-
|
|
25
|
-
### Command
|
|
26
|
-
\`\`\`bash
|
|
27
|
-
${fence(rawCommand)}
|
|
28
|
-
\`\`\``
|
|
29
|
-
: '';
|
|
30
42
|
const logLine = logAttachmentAttempted ? 'Log attachment was attempted but failed. Check the solver terminal log for the complete failure output.' : 'Logs were not attached because `--attach-logs` was not enabled.';
|
|
43
|
+
const actionSection = buildPrePullRequestFailureActionSection(reason);
|
|
31
44
|
|
|
32
45
|
return `## 🚨 ${SOLUTION_DRAFT_FAILED_MARKER}
|
|
33
46
|
|
|
@@ -41,11 +54,12 @@ The automated solver stopped before creating a pull request, so no PR was opened
|
|
|
41
54
|
**Reason**
|
|
42
55
|
\`\`\`text
|
|
43
56
|
${fence(reason)}
|
|
44
|
-
|
|
57
|
+
\`\`\`
|
|
45
58
|
|
|
46
|
-
${
|
|
59
|
+
${actionSection}
|
|
47
60
|
|
|
48
|
-
|
|
61
|
+
${logLine}
|
|
62
|
+
`;
|
|
49
63
|
}
|
|
50
64
|
|
|
51
65
|
export async function notifyIssueAboutPrePullRequestFailure(options) {
|
|
@@ -553,8 +553,9 @@ export const setupRepository = async (argv, owner, repo, forkOwner = null, issue
|
|
|
553
553
|
if (deleteResult.code !== 0) {
|
|
554
554
|
const delOut = (deleteResult.stderr?.toString() || '') + (deleteResult.stdout?.toString() || '');
|
|
555
555
|
await log(`${formatAligned('❌', 'Delete failed:', delOut.split('\n')[0])}`, { level: 'error' });
|
|
556
|
-
await log(` 💡 Manual fix: gh repo delete ${existingForkName} --yes, then re-run`);
|
|
557
|
-
await
|
|
556
|
+
await log(/delete_repo/i.test(delOut) || (/HTTP 403/.test(delOut) && /admin rights/i.test(delOut)) ? ` 💡 Token missing "delete_repo" scope. Hive Mind does not request it by default. Admin fix: gh auth refresh -h github.com -s delete_repo\n 🔧 Or no-scope alternative: gh repo rename ${existingForkName} ${existingForkName.split('/')[1]}-old (or gh repo archive ${existingForkName} --yes), then re-run with --prefix-fork-name-with-owner-name` : ` 💡 Manual fix: gh repo delete ${existingForkName} --yes, then re-run`); // Issue #1651
|
|
557
|
+
if (argv.verbose) await log(`${formatAligned('🔧', 'Full delete output:', delOut.trim())}`);
|
|
558
|
+
await safeExit(1, `Auto-recovery failed - ask the fork owner or Hive Mind administrator to delete, rename, or archive ${existingForkName}`);
|
|
558
559
|
}
|
|
559
560
|
await log(`${formatAligned('✅', 'Deleted:', existingForkName)}`);
|
|
560
561
|
existingForkName = null; // Fall through to fork creation below
|