@link-assistant/hive-mind 1.54.3 → 1.54.5
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 +12 -0
- package/package.json +4 -3
- package/src/claude-quiet-config.lib.mjs +146 -0
- package/src/claude.lib.mjs +2 -0
- package/src/config.lib.mjs +3 -2
- package/src/configure-claude.lib.mjs +127 -0
- package/src/configure-claude.mjs +35 -0
- package/src/exit-handler.lib.mjs +22 -1
- package/src/solve.error-handlers.lib.mjs +1 -0
- package/src/solve.mjs +2 -1
- package/src/solve.pre-pr-failure-notifier.lib.mjs +107 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.54.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ea79845: Disable noisy Claude Code features for solve runs via merged user settings, subprocess environment variables, and Docker image defaults. Expands the quiet config to also disable fast mode, feedback surveys, mouse tracking, away summaries, Claude attribution (commit/pr), co-authored-by trailer, thinking summaries, and UI animations, sets viewMode to verbose, and caps tool-use concurrency at 4 for deterministic autonomous runs. Keeps Claude's built-in git/PR instructions on (`includeGitInstructions: true`), enables task tracking (`CLAUDE_CODE_ENABLE_TASKS=1`) and turn resume (`CLAUDE_CODE_RESUME_INTERRUPTED_TURN=1`), and makes the bypass-permissions mode audible via `permissions.defaultMode: "bypassPermissions"` + `skipDangerousModePermissionPrompt: true` (complementing the existing `--dangerously-skip-permissions` CLI flag). Adds a reusable `configure-claude` bin with an apply default and a `--verify` check-only mode so users and system administrators can reset or audit Claude Code configuration manually after installing `@link-assistant/hive-mind`. Docker release builds now wait for the npm package version to become available, pass that exact version into Docker as `HIVE_MIND_VERSION`, install `@link-assistant/hive-mind@${HIVE_MIND_VERSION}`, and invoke the published `configure-claude` bin directly instead of copying repo source files into the Docker build.
|
|
8
|
+
|
|
9
|
+
## 1.54.4
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 2ac0a14: Notify the source issue when solve exits with a known issue but no pull request, including failure logs when `--attach-logs` is enabled.
|
|
14
|
+
|
|
3
15
|
## 1.54.3
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/hive-mind",
|
|
3
|
-
"version": "1.54.
|
|
3
|
+
"version": "1.54.5",
|
|
4
4
|
"description": "AI-powered issue solver and hive mind for collaborative problem solving",
|
|
5
5
|
"main": "src/hive.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -9,11 +9,12 @@
|
|
|
9
9
|
"solve": "./src/solve.mjs",
|
|
10
10
|
"task": "./src/task.mjs",
|
|
11
11
|
"review": "./src/review.mjs",
|
|
12
|
+
"configure-claude": "./src/configure-claude.mjs",
|
|
12
13
|
"start-screen": "./src/start-screen.mjs",
|
|
13
14
|
"hive-telegram-bot": "./src/telegram-bot.mjs"
|
|
14
15
|
},
|
|
15
16
|
"scripts": {
|
|
16
|
-
"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-issue-1616-pr-issue-link-preservation.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",
|
|
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-issue-1616-pr-issue-link-preservation.mjs && node tests/test-pre-pr-failure-notifier-1640.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",
|
|
17
18
|
"test:queue": "node tests/solve-queue.test.mjs",
|
|
18
19
|
"test:limits-display": "node tests/limits-display.test.mjs",
|
|
19
20
|
"test:usage-limit": "node tests/test-usage-limit.mjs",
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
"changeset": "changeset",
|
|
26
27
|
"changeset:version": "changeset version",
|
|
27
28
|
"changeset:publish": "npm run build:pre && changeset publish",
|
|
28
|
-
"build:pre": "chmod +x src/hive.mjs && chmod +x src/solve.mjs",
|
|
29
|
+
"build:pre": "chmod +x src/hive.mjs && chmod +x src/solve.mjs && chmod +x src/configure-claude.mjs",
|
|
29
30
|
"prepare": "husky"
|
|
30
31
|
},
|
|
31
32
|
"repository": {
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
export const REQUIRED_CLAUDE_QUIET_ENV = Object.freeze({
|
|
8
|
+
CLAUDE_CODE_DISABLE_AUTO_MEMORY: '1',
|
|
9
|
+
CLAUDE_CODE_DISABLE_CRON: '1',
|
|
10
|
+
CLAUDE_CODE_DISABLE_TERMINAL_TITLE: '1',
|
|
11
|
+
CLAUDE_CODE_DISABLE_CLAUDE_MDS: '1',
|
|
12
|
+
CLAUDE_CODE_DISABLE_FAST_MODE: '1',
|
|
13
|
+
CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY: '1',
|
|
14
|
+
CLAUDE_CODE_DISABLE_MOUSE: '1',
|
|
15
|
+
CLAUDE_CODE_ENABLE_AWAY_SUMMARY: '0',
|
|
16
|
+
CLAUDE_CODE_ENABLE_TASKS: '1',
|
|
17
|
+
CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY: '4',
|
|
18
|
+
CLAUDE_CODE_RESUME_INTERRUPTED_TURN: '1',
|
|
19
|
+
DISABLE_FEEDBACK_COMMAND: '1',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const REQUIRED_CLAUDE_QUIET_SETTINGS = Object.freeze({
|
|
23
|
+
autoMemoryEnabled: false,
|
|
24
|
+
spinnerTipsEnabled: false,
|
|
25
|
+
awaySummaryEnabled: false,
|
|
26
|
+
feedbackSurveyRate: 0,
|
|
27
|
+
includeCoAuthoredBy: false,
|
|
28
|
+
includeGitInstructions: true,
|
|
29
|
+
prefersReducedMotion: true,
|
|
30
|
+
showThinkingSummaries: false,
|
|
31
|
+
skipDangerousModePermissionPrompt: true,
|
|
32
|
+
viewMode: 'verbose',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const REQUIRED_CLAUDE_QUIET_ATTRIBUTION = Object.freeze({
|
|
36
|
+
commit: '',
|
|
37
|
+
pr: '',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const REQUIRED_CLAUDE_QUIET_PERMISSIONS = Object.freeze({
|
|
41
|
+
defaultMode: 'bypassPermissions',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const buildClaudeQuietEnv = (baseEnv = process.env) => ({
|
|
45
|
+
...baseEnv,
|
|
46
|
+
...REQUIRED_CLAUDE_QUIET_ENV,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const formatClaudeQuietConfigSummary = () => {
|
|
50
|
+
const settings = Object.entries(REQUIRED_CLAUDE_QUIET_SETTINGS)
|
|
51
|
+
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
|
|
52
|
+
.join(', ');
|
|
53
|
+
const attribution = `attribution=${JSON.stringify(REQUIRED_CLAUDE_QUIET_ATTRIBUTION)}`;
|
|
54
|
+
const permissions = `permissions=${JSON.stringify(REQUIRED_CLAUDE_QUIET_PERMISSIONS)}`;
|
|
55
|
+
const env = Object.entries(REQUIRED_CLAUDE_QUIET_ENV)
|
|
56
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
57
|
+
.join(', ');
|
|
58
|
+
return `settings[${settings}, ${attribution}, ${permissions}], env[${env}]`;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const isPlainObject = value => value && typeof value === 'object' && !Array.isArray(value);
|
|
62
|
+
|
|
63
|
+
export const ensureClaudeQuietConfig = async ({ settingsPath, log } = {}) => {
|
|
64
|
+
const resolvedPath = settingsPath || path.join(os.homedir(), '.claude', 'settings.json');
|
|
65
|
+
let settings = {};
|
|
66
|
+
try {
|
|
67
|
+
const content = await fs.readFile(resolvedPath, 'utf-8');
|
|
68
|
+
const parsed = JSON.parse(content);
|
|
69
|
+
settings = isPlainObject(parsed) ? parsed : {};
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (err.code !== 'ENOENT' && log) {
|
|
72
|
+
await log(`⚠️ Could not read ${resolvedPath}: ${err.message}`, { verbose: true });
|
|
73
|
+
}
|
|
74
|
+
settings = {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const updatedSettingsKeys = [];
|
|
78
|
+
for (const [key, value] of Object.entries(REQUIRED_CLAUDE_QUIET_SETTINGS)) {
|
|
79
|
+
if (settings[key] !== value) {
|
|
80
|
+
settings[key] = value;
|
|
81
|
+
updatedSettingsKeys.push(key);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const existingAttribution = isPlainObject(settings.attribution) ? settings.attribution : {};
|
|
86
|
+
const updatedAttributionKeys = [];
|
|
87
|
+
for (const [key, value] of Object.entries(REQUIRED_CLAUDE_QUIET_ATTRIBUTION)) {
|
|
88
|
+
if (existingAttribution[key] !== value) {
|
|
89
|
+
existingAttribution[key] = value;
|
|
90
|
+
updatedAttributionKeys.push(key);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
settings.attribution = existingAttribution;
|
|
94
|
+
if (updatedAttributionKeys.length > 0) {
|
|
95
|
+
updatedSettingsKeys.push('attribution');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const existingPermissions = isPlainObject(settings.permissions) ? settings.permissions : {};
|
|
99
|
+
const updatedPermissionsKeys = [];
|
|
100
|
+
for (const [key, value] of Object.entries(REQUIRED_CLAUDE_QUIET_PERMISSIONS)) {
|
|
101
|
+
if (existingPermissions[key] !== value) {
|
|
102
|
+
existingPermissions[key] = value;
|
|
103
|
+
updatedPermissionsKeys.push(key);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
settings.permissions = existingPermissions;
|
|
107
|
+
if (updatedPermissionsKeys.length > 0) {
|
|
108
|
+
updatedSettingsKeys.push('permissions');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const existingEnv = isPlainObject(settings.env) ? settings.env : {};
|
|
112
|
+
const updatedEnvKeys = [];
|
|
113
|
+
for (const [key, value] of Object.entries(REQUIRED_CLAUDE_QUIET_ENV)) {
|
|
114
|
+
if (existingEnv[key] !== value) {
|
|
115
|
+
existingEnv[key] = value;
|
|
116
|
+
updatedEnvKeys.push(key);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
settings.env = existingEnv;
|
|
120
|
+
|
|
121
|
+
const changed = updatedSettingsKeys.length > 0 || updatedEnvKeys.length > 0;
|
|
122
|
+
try {
|
|
123
|
+
if (changed) {
|
|
124
|
+
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
125
|
+
await fs.writeFile(resolvedPath, JSON.stringify(settings, null, 2));
|
|
126
|
+
}
|
|
127
|
+
if (log) {
|
|
128
|
+
await log(`🧭 Claude Code quiet config ${changed ? 'updated' : 'verified'} at ${resolvedPath}: ${formatClaudeQuietConfigSummary()}`);
|
|
129
|
+
}
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (log) await log(`⚠️ Could not write ${resolvedPath}: ${err.message}`, { verbose: true });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
path: resolvedPath,
|
|
136
|
+
changed,
|
|
137
|
+
updatedSettingsKeys,
|
|
138
|
+
updatedEnvKeys,
|
|
139
|
+
updatedAttributionKeys,
|
|
140
|
+
updatedPermissionsKeys,
|
|
141
|
+
settings: { ...REQUIRED_CLAUDE_QUIET_SETTINGS },
|
|
142
|
+
attribution: { ...REQUIRED_CLAUDE_QUIET_ATTRIBUTION },
|
|
143
|
+
permissions: { ...REQUIRED_CLAUDE_QUIET_PERMISSIONS },
|
|
144
|
+
env: { ...REQUIRED_CLAUDE_QUIET_ENV },
|
|
145
|
+
};
|
|
146
|
+
};
|
package/src/claude.lib.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs'; //
|
|
|
21
21
|
import { CLAUDE_MODELS as availableModels } from './models/index.mjs'; // Issue #1221
|
|
22
22
|
import { buildMcpConfigWithoutPlaywright } from './playwright-mcp.lib.mjs';
|
|
23
23
|
import { resolveClaudeSessionToolFlags } from './useless-tools.lib.mjs';
|
|
24
|
+
import { ensureClaudeQuietConfig } from './claude-quiet-config.lib.mjs';
|
|
24
25
|
import { fetchModelInfo } from './model-info.lib.mjs';
|
|
25
26
|
export { availableModels }; // Re-export for backward compatibility
|
|
26
27
|
export { fetchModelInfo };
|
|
@@ -725,6 +726,7 @@ export const executeClaudeCommand = async params => {
|
|
|
725
726
|
await log(`🔄 Resuming from session: ${argv.resume}`);
|
|
726
727
|
claudeArgs = `--resume ${argv.resume} ${claudeArgs}`;
|
|
727
728
|
}
|
|
729
|
+
await ensureClaudeQuietConfig({ log });
|
|
728
730
|
const { mcpConfigPath, disallowedToolsList } = await resolveClaudeSessionToolFlags({ argv, log, fallbackBuildMcpConfigWithoutPlaywright: buildMcpConfigWithoutPlaywright });
|
|
729
731
|
if (mcpConfigPath) claudeArgs += ` --strict-mcp-config --mcp-config "${mcpConfigPath}"`;
|
|
730
732
|
if (disallowedToolsList.length) claudeArgs += ` --disallowedTools ${disallowedToolsList.join(' ')}`;
|
package/src/config.lib.mjs
CHANGED
|
@@ -24,6 +24,7 @@ const getenv = typeof getenvModule === 'function' ? getenvModule : getenvModule.
|
|
|
24
24
|
|
|
25
25
|
// Use semver package for version comparison (see issue #1146)
|
|
26
26
|
import semver from 'semver';
|
|
27
|
+
import { buildClaudeQuietEnv } from './claude-quiet-config.lib.mjs';
|
|
27
28
|
|
|
28
29
|
// Import lino for parsing Links Notation format
|
|
29
30
|
const { lino } = await import('./lino.lib.mjs');
|
|
@@ -419,14 +420,14 @@ export const getClaudeEnv = (options = {}) => {
|
|
|
419
420
|
// Get max output tokens based on model (Issue #1221)
|
|
420
421
|
const maxOutputTokens = options.model ? getMaxOutputTokensForModel(options.model) : claudeCode.maxOutputTokens;
|
|
421
422
|
|
|
422
|
-
const env = {
|
|
423
|
+
const env = buildClaudeQuietEnv({
|
|
423
424
|
...process.env,
|
|
424
425
|
CLAUDE_CODE_MAX_OUTPUT_TOKENS: String(maxOutputTokens),
|
|
425
426
|
// MCP timeout configurations to prevent tool calls from hanging indefinitely
|
|
426
427
|
// See: https://github.com/link-assistant/hive-mind/issues/1066
|
|
427
428
|
MCP_TIMEOUT: String(claudeCode.mcpTimeout),
|
|
428
429
|
MCP_TOOL_TIMEOUT: String(claudeCode.mcpToolTimeout),
|
|
429
|
-
};
|
|
430
|
+
});
|
|
430
431
|
|
|
431
432
|
// Opus 4.7+ always uses adaptive thinking — MAX_THINKING_TOKENS has no effect (Issue #1620)
|
|
432
433
|
// For Opus 4.6 and earlier, MAX_THINKING_TOKENS controls extended thinking (Claude Code >= 2.1.12)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared runner for the `configure-claude` bin command. Applies or verifies
|
|
5
|
+
* the quiet Claude Code defaults in a target `settings.json` by reusing the
|
|
6
|
+
* canonical maps and idempotent merge helpers from:
|
|
7
|
+
* - src/claude-quiet-config.lib.mjs (quiet env + settings + attribution
|
|
8
|
+
* + permissions)
|
|
9
|
+
* - src/useless-tools.lib.mjs (disallowedTools block-list)
|
|
10
|
+
*
|
|
11
|
+
* See issues #1627 and #1642.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs/promises';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
|
|
18
|
+
import { REQUIRED_CLAUDE_QUIET_ENV, REQUIRED_CLAUDE_QUIET_SETTINGS, REQUIRED_CLAUDE_QUIET_ATTRIBUTION, REQUIRED_CLAUDE_QUIET_PERMISSIONS, ensureClaudeQuietConfig } from './claude-quiet-config.lib.mjs';
|
|
19
|
+
import { buildDisallowedToolsList, ensureDisallowedToolsInSettings } from './useless-tools.lib.mjs';
|
|
20
|
+
|
|
21
|
+
export const resolveSettingsPath = settingsPath => settingsPath || path.join(os.homedir(), '.claude', 'settings.json');
|
|
22
|
+
|
|
23
|
+
export const parseConfigureClaudeArgs = argv => {
|
|
24
|
+
const args = { settingsPath: null, verify: false, help: false };
|
|
25
|
+
for (let i = 0; i < argv.length; i++) {
|
|
26
|
+
const arg = argv[i];
|
|
27
|
+
if (arg === '--settings-path' || arg === '-s') {
|
|
28
|
+
args.settingsPath = argv[++i];
|
|
29
|
+
} else if (arg.startsWith('--settings-path=')) {
|
|
30
|
+
args.settingsPath = arg.slice('--settings-path='.length);
|
|
31
|
+
} else if (arg === '--verify') {
|
|
32
|
+
args.verify = true;
|
|
33
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
34
|
+
args.help = true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return args;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const CONFIGURE_CLAUDE_HELP = `Usage: configure-claude [options]
|
|
41
|
+
|
|
42
|
+
Apply or verify Hive-Mind's quiet, deterministic Claude Code defaults in
|
|
43
|
+
a target ~/.claude/settings.json (env vars, settings, attribution,
|
|
44
|
+
permissions.defaultMode, and the disallowedTools block-list).
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
-s, --settings-path <path> Path to settings.json (default: ~/.claude/settings.json)
|
|
48
|
+
--verify Report configuration status without writing; exit 1 if incorrect
|
|
49
|
+
-h, --help Show this help and exit
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
configure-claude # apply defaults to ~/.claude/settings.json
|
|
53
|
+
configure-claude --verify # check only, non-zero exit if drift detected
|
|
54
|
+
configure-claude -s /workspace/.claude/settings.json
|
|
55
|
+
|
|
56
|
+
Reference: https://github.com/link-assistant/hive-mind/issues/1642
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const isPlainObject = value => value && typeof value === 'object' && !Array.isArray(value);
|
|
60
|
+
|
|
61
|
+
const readSettings = async settingsPath => {
|
|
62
|
+
try {
|
|
63
|
+
const content = await fs.readFile(settingsPath, 'utf-8');
|
|
64
|
+
const parsed = JSON.parse(content);
|
|
65
|
+
return isPlainObject(parsed) ? parsed : {};
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err.code === 'ENOENT') return null;
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const verifyConfigureClaude = async ({ settingsPath } = {}) => {
|
|
73
|
+
const resolvedPath = resolveSettingsPath(settingsPath);
|
|
74
|
+
const settings = await readSettings(resolvedPath);
|
|
75
|
+
const missing = {
|
|
76
|
+
file: settings === null,
|
|
77
|
+
settings: [],
|
|
78
|
+
env: [],
|
|
79
|
+
attribution: [],
|
|
80
|
+
permissions: [],
|
|
81
|
+
disallowedTools: [],
|
|
82
|
+
};
|
|
83
|
+
const current = settings || {};
|
|
84
|
+
for (const [key, value] of Object.entries(REQUIRED_CLAUDE_QUIET_SETTINGS)) {
|
|
85
|
+
if (current[key] !== value) missing.settings.push(key);
|
|
86
|
+
}
|
|
87
|
+
const envSection = isPlainObject(current.env) ? current.env : {};
|
|
88
|
+
for (const [key, value] of Object.entries(REQUIRED_CLAUDE_QUIET_ENV)) {
|
|
89
|
+
if (envSection[key] !== value) missing.env.push(key);
|
|
90
|
+
}
|
|
91
|
+
const attributionSection = isPlainObject(current.attribution) ? current.attribution : {};
|
|
92
|
+
for (const [key, value] of Object.entries(REQUIRED_CLAUDE_QUIET_ATTRIBUTION)) {
|
|
93
|
+
if (attributionSection[key] !== value) missing.attribution.push(key);
|
|
94
|
+
}
|
|
95
|
+
const permissionsSection = isPlainObject(current.permissions) ? current.permissions : {};
|
|
96
|
+
for (const [key, value] of Object.entries(REQUIRED_CLAUDE_QUIET_PERMISSIONS)) {
|
|
97
|
+
if (permissionsSection[key] !== value) missing.permissions.push(key);
|
|
98
|
+
}
|
|
99
|
+
const existingDisallowed = Array.isArray(current.disallowedTools) ? current.disallowedTools : [];
|
|
100
|
+
for (const required of buildDisallowedToolsList()) {
|
|
101
|
+
if (!existingDisallowed.includes(required)) missing.disallowedTools.push(required);
|
|
102
|
+
}
|
|
103
|
+
const ok = !missing.file && missing.settings.length === 0 && missing.env.length === 0 && missing.attribution.length === 0 && missing.permissions.length === 0 && missing.disallowedTools.length === 0;
|
|
104
|
+
return { ok, path: resolvedPath, missing };
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const runConfigureClaude = async ({ settingsPath, log } = {}) => {
|
|
108
|
+
const resolvedPath = resolveSettingsPath(settingsPath);
|
|
109
|
+
const logger = log || (async line => console.log(line));
|
|
110
|
+
const quietResult = await ensureClaudeQuietConfig({ settingsPath: resolvedPath, log: logger });
|
|
111
|
+
const disallowedResult = await ensureDisallowedToolsInSettings({ settingsPath: resolvedPath, log: logger });
|
|
112
|
+
return { quietResult, disallowedResult, path: resolvedPath };
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const formatVerifyReport = ({ ok, path: resolvedPath, missing }) => {
|
|
116
|
+
if (ok) {
|
|
117
|
+
return `✅ Quiet Claude Code configuration is up to date in ${resolvedPath}`;
|
|
118
|
+
}
|
|
119
|
+
const sections = [];
|
|
120
|
+
if (missing.file) sections.push(' - settings.json missing');
|
|
121
|
+
if (missing.settings.length) sections.push(` - settings: ${missing.settings.join(', ')}`);
|
|
122
|
+
if (missing.env.length) sections.push(` - env: ${missing.env.join(', ')}`);
|
|
123
|
+
if (missing.attribution.length) sections.push(` - attribution: ${missing.attribution.join(', ')}`);
|
|
124
|
+
if (missing.permissions.length) sections.push(` - permissions: ${missing.permissions.join(', ')}`);
|
|
125
|
+
if (missing.disallowedTools.length) sections.push(` - disallowedTools: ${missing.disallowedTools.join(', ')}`);
|
|
126
|
+
return `❌ Quiet Claude Code configuration drift detected in ${resolvedPath}\n${sections.join('\n')}`;
|
|
127
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `configure-claude` — reusable bin that resets Hive-Mind's quiet,
|
|
5
|
+
* deterministic Claude Code defaults (env, settings, attribution,
|
|
6
|
+
* permissions.defaultMode, and the disallowedTools block-list) in a
|
|
7
|
+
* target `settings.json`, or verifies that they are already in place.
|
|
8
|
+
*
|
|
9
|
+
* Users and system administrators can run this manually after installing
|
|
10
|
+
* `@link-assistant/hive-mind` to reset Claude Code configuration. Docker
|
|
11
|
+
* images invoke this published bin after npm release, so the image baseline
|
|
12
|
+
* stays in lock-step with the package users install.
|
|
13
|
+
*
|
|
14
|
+
* See issues #1627 and #1642.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { CONFIGURE_CLAUDE_HELP, formatVerifyReport, parseConfigureClaudeArgs, resolveSettingsPath, runConfigureClaude, verifyConfigureClaude } from './configure-claude.lib.mjs';
|
|
18
|
+
|
|
19
|
+
const args = parseConfigureClaudeArgs(process.argv.slice(2));
|
|
20
|
+
|
|
21
|
+
if (args.help) {
|
|
22
|
+
console.log(CONFIGURE_CLAUDE_HELP);
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const settingsPath = resolveSettingsPath(args.settingsPath);
|
|
27
|
+
|
|
28
|
+
if (args.verify) {
|
|
29
|
+
const report = await verifyConfigureClaude({ settingsPath });
|
|
30
|
+
console.log(formatVerifyReport(report));
|
|
31
|
+
process.exit(report.ok ? 0 : 1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { quietResult, disallowedResult } = await runConfigureClaude({ settingsPath });
|
|
35
|
+
console.log(`Configured quiet Claude Code defaults and ${disallowedResult.total} disallowedTools in ${quietResult.path}`);
|
package/src/exit-handler.lib.mjs
CHANGED
|
@@ -27,6 +27,8 @@ let logFunction = null;
|
|
|
27
27
|
let cleanupFunction = null;
|
|
28
28
|
let interruptFunction = null;
|
|
29
29
|
let interruptHandlerRan = false;
|
|
30
|
+
let preExitFunction = null;
|
|
31
|
+
let preExitHandlerRan = false;
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
34
|
* Initialize the exit handler with required dependencies
|
|
@@ -36,11 +38,16 @@ let interruptHandlerRan = false;
|
|
|
36
38
|
* @param {Function} interrupt - Optional interrupt function to call on SIGINT/SIGTERM before cleanup
|
|
37
39
|
* (e.g., auto-commit uncommitted changes, upload logs)
|
|
38
40
|
*/
|
|
39
|
-
export const initializeExitHandler = (getLogPath, log, cleanup = null, interrupt = null) => {
|
|
41
|
+
export const initializeExitHandler = (getLogPath, log, cleanup = null, interrupt = null, preExit = null) => {
|
|
40
42
|
getLogPathFunction = getLogPath;
|
|
41
43
|
logFunction = log;
|
|
42
44
|
cleanupFunction = cleanup;
|
|
43
45
|
interruptFunction = interrupt;
|
|
46
|
+
preExitFunction = preExit;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const setPreExitHandler = preExit => {
|
|
50
|
+
preExitFunction = preExit;
|
|
44
51
|
};
|
|
45
52
|
|
|
46
53
|
/**
|
|
@@ -200,6 +207,20 @@ export const logActiveHandles = async (log = null) => {
|
|
|
200
207
|
export const safeExit = async (code = 0, reason = 'Process completed') => {
|
|
201
208
|
await showExitMessage(reason, code);
|
|
202
209
|
|
|
210
|
+
if (code !== 0 && preExitFunction && !preExitHandlerRan) {
|
|
211
|
+
preExitHandlerRan = true;
|
|
212
|
+
try {
|
|
213
|
+
await preExitFunction({ code, reason });
|
|
214
|
+
} catch (error) {
|
|
215
|
+
const message = error && error.message ? error.message : String(error);
|
|
216
|
+
if (logFunction) {
|
|
217
|
+
await logFunction(`⚠️ Pre-exit handler failed: ${message}`, { level: 'warning' });
|
|
218
|
+
} else {
|
|
219
|
+
console.warn(`⚠️ Pre-exit handler failed: ${message}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
203
224
|
// Issue #1431: Drain/unref active handles so the event loop exits naturally.
|
|
204
225
|
// This resolves the root causes of dangling ReadStream (stdin), Socket (undici),
|
|
205
226
|
// ChildProcess (command-stream), and WriteStream (stdout/stderr) handles.
|
|
@@ -70,6 +70,7 @@ export const handleFailure = async options => {
|
|
|
70
70
|
});
|
|
71
71
|
if (logUploadSuccess) {
|
|
72
72
|
await log(`📎 Failure log attached to ${targetLabel}`);
|
|
73
|
+
if (!hasPR && hasIssue) global.prePullRequestFailureNotificationPosted = true;
|
|
73
74
|
}
|
|
74
75
|
} catch (attachError) {
|
|
75
76
|
reportError(attachError, {
|
package/src/solve.mjs
CHANGED
|
@@ -41,6 +41,7 @@ const { formatResetTimeWithRelative } = usageLimitLib;
|
|
|
41
41
|
|
|
42
42
|
const errorHandlers = await import('./solve.error-handlers.lib.mjs');
|
|
43
43
|
const { createUncaughtExceptionHandler, createUnhandledRejectionHandler, handleMainExecutionError, handleNoPrAvailableError } = errorHandlers;
|
|
44
|
+
const { notifyIssueAboutPrePullRequestFailure } = await import('./solve.pre-pr-failure-notifier.lib.mjs');
|
|
44
45
|
|
|
45
46
|
const watchLib = await import('./solve.watch.lib.mjs');
|
|
46
47
|
const { startWatchMode } = watchLib;
|
|
@@ -132,7 +133,7 @@ const cleanupWrapper = async () => {
|
|
|
132
133
|
}
|
|
133
134
|
};
|
|
134
135
|
const interruptWrapper = createInterruptWrapper({ cleanupContext, checkForUncommittedChanges, shouldAttachLogs, attachLogToGitHub, getLogFile, sanitizeLogContent, $, log });
|
|
135
|
-
initializeExitHandler(getAbsoluteLogPath, log, cleanupWrapper, interruptWrapper);
|
|
136
|
+
initializeExitHandler(getAbsoluteLogPath, log, cleanupWrapper, interruptWrapper, ({ code, reason }) => notifyIssueAboutPrePullRequestFailure({ code, reason, argv, globalState: global, $, log, getLogFile, shouldAttachLogs, attachLogToGitHub, sanitizeLogContent, rawCommand }));
|
|
136
137
|
installGlobalExitHandlers();
|
|
137
138
|
|
|
138
139
|
// Now handle argument validation that was moved from early checks
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { getTrackedToolCommentIds, postTrackedComment, SOLUTION_DRAFT_FAILED_MARKER } from './tool-comments.lib.mjs';
|
|
2
|
+
|
|
3
|
+
const truncate = (value, maxLength = 2000) => {
|
|
4
|
+
const text = value === null || value === undefined ? '' : String(value);
|
|
5
|
+
if (text.length <= maxLength) return text;
|
|
6
|
+
return `${text.slice(0, maxLength - 22)}\n... truncated ...`;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const fence = value => truncate(value || 'Unknown error').replaceAll('```', '` ` `');
|
|
10
|
+
|
|
11
|
+
export function shouldNotifyIssueAboutPrePullRequestFailure({ code, globalState }) {
|
|
12
|
+
if (code === 0) return false;
|
|
13
|
+
if (!globalState?.issueNumber || !globalState?.owner || !globalState?.repo) return false;
|
|
14
|
+
if (globalState?.createdPR?.number) return false;
|
|
15
|
+
if (globalState.prePullRequestFailureNotificationPosted || globalState.prePullRequestFailureNotificationInProgress) return false;
|
|
16
|
+
return getTrackedToolCommentIds().size === 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildPrePullRequestFailureComment({ reason, owner, repo, issueNumber, argv = {}, rawCommand = null, logAttachmentAttempted = false }) {
|
|
20
|
+
const tool = argv.tool || 'claude';
|
|
21
|
+
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
|
+
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.';
|
|
31
|
+
|
|
32
|
+
return `## 🚨 ${SOLUTION_DRAFT_FAILED_MARKER}
|
|
33
|
+
|
|
34
|
+
The automated solver stopped before creating a pull request, so no PR was opened for this issue.
|
|
35
|
+
|
|
36
|
+
### Failure
|
|
37
|
+
- **Repository**: \`${owner}/${repo}\`
|
|
38
|
+
- **Issue**: #${issueNumber}
|
|
39
|
+
- **Tool**: \`${tool}\`${modelLine}
|
|
40
|
+
|
|
41
|
+
**Reason**
|
|
42
|
+
\`\`\`text
|
|
43
|
+
${fence(reason)}
|
|
44
|
+
\`\`\`${commandBlock}
|
|
45
|
+
|
|
46
|
+
${logLine}
|
|
47
|
+
|
|
48
|
+
Please resolve the reported problem and rerun the solve command.`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function notifyIssueAboutPrePullRequestFailure(options) {
|
|
52
|
+
const { code, reason, argv = {}, globalState = globalThis, $, log = async () => {}, getLogFile, shouldAttachLogs = false, attachLogToGitHub, sanitizeLogContent, rawCommand = null, postComment = postTrackedComment } = options;
|
|
53
|
+
|
|
54
|
+
if (!shouldNotifyIssueAboutPrePullRequestFailure({ code, globalState })) {
|
|
55
|
+
return { notified: false, skipped: true };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const owner = globalState.owner;
|
|
59
|
+
const repo = globalState.repo;
|
|
60
|
+
const issueNumber = globalState.issueNumber;
|
|
61
|
+
globalState.prePullRequestFailureNotificationInProgress = true;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
if (shouldAttachLogs && getLogFile && attachLogToGitHub && sanitizeLogContent) {
|
|
65
|
+
await log(`\n📄 Notifying issue #${issueNumber} about pre-PR failure with logs...`);
|
|
66
|
+
const uploaded = await attachLogToGitHub({
|
|
67
|
+
logFile: getLogFile(),
|
|
68
|
+
targetType: 'issue',
|
|
69
|
+
targetNumber: issueNumber,
|
|
70
|
+
owner,
|
|
71
|
+
repo,
|
|
72
|
+
$,
|
|
73
|
+
log,
|
|
74
|
+
sanitizeLogContent,
|
|
75
|
+
verbose: argv.verbose,
|
|
76
|
+
errorMessage: `The solver stopped before creating a pull request.\n\nReason: ${reason || 'Unknown error'}`,
|
|
77
|
+
requestedModel: argv.model,
|
|
78
|
+
tool: argv.tool || 'claude',
|
|
79
|
+
});
|
|
80
|
+
if (uploaded) {
|
|
81
|
+
globalState.prePullRequestFailureNotificationPosted = true;
|
|
82
|
+
return { notified: true, method: 'log-upload' };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await log(`\n💬 Notifying issue #${issueNumber} about pre-PR failure...`);
|
|
87
|
+
const body = buildPrePullRequestFailureComment({
|
|
88
|
+
reason,
|
|
89
|
+
owner,
|
|
90
|
+
repo,
|
|
91
|
+
issueNumber,
|
|
92
|
+
argv,
|
|
93
|
+
rawCommand,
|
|
94
|
+
logAttachmentAttempted: shouldAttachLogs,
|
|
95
|
+
});
|
|
96
|
+
const posted = await postComment({ $, owner, repo, targetNumber: issueNumber, body });
|
|
97
|
+
if (posted.ok) {
|
|
98
|
+
globalState.prePullRequestFailureNotificationPosted = true;
|
|
99
|
+
await log(` ✅ Pre-PR failure comment posted to issue #${issueNumber}${posted.commentId ? ` (id=${posted.commentId})` : ''}`);
|
|
100
|
+
return { notified: true, method: 'comment', commentId: posted.commentId || null };
|
|
101
|
+
}
|
|
102
|
+
await log(` ⚠️ Could not post pre-PR failure comment: ${posted.stderr || 'unknown error'}`, { level: 'warning' });
|
|
103
|
+
return { notified: false, error: posted.stderr || 'unknown error' };
|
|
104
|
+
} finally {
|
|
105
|
+
globalState.prePullRequestFailureNotificationInProgress = false;
|
|
106
|
+
}
|
|
107
|
+
}
|