helloagents 3.0.22 → 3.0.25
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +19 -11
- package/README_CN.md +19 -11
- package/bootstrap-lite.md +2 -2
- package/bootstrap.md +2 -2
- package/gemini-extension.json +1 -1
- package/install.ps1 +11 -11
- package/package.json +1 -1
- package/scripts/cli-codex-config.mjs +50 -3
- package/scripts/cli-codex-hooks-state.mjs +264 -0
- package/scripts/cli-codex.mjs +21 -14
- package/scripts/cli-doctor-codex.mjs +26 -3
- package/scripts/cli-host-detect.mjs +3 -3
- package/scripts/cli-lifecycle.mjs +14 -7
- package/scripts/cli-messages.mjs +1 -1
- package/scripts/cli-utils.mjs +4 -3
- package/scripts/delivery-gate.mjs +20 -11
- package/scripts/notify-closeout.mjs +22 -2
- package/scripts/notify-route.mjs +22 -15
- package/scripts/notify-sound.mjs +94 -0
- package/scripts/notify-ui.mjs +43 -11
- package/scripts/notify.mjs +241 -66
- package/scripts/project-session-cleanup.mjs +27 -1
- package/scripts/ralph-loop.mjs +76 -81
- package/scripts/runtime-scope.mjs +45 -17
- package/scripts/session-capsule.mjs +1 -0
- package/scripts/turn-state-cli.mjs +24 -2
- package/scripts/turn-stop-gate.mjs +61 -7
- package/skills/commands/help/SKILL.md +1 -1
- package/skills/helloagents/SKILL.md +1 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
import { isTomlTableHeader, normalizeToml } from './cli-toml.mjs'
|
|
4
|
+
import { removeIfExists, safeJson, safeRead, safeWrite } from './cli-utils.mjs'
|
|
5
|
+
|
|
6
|
+
const MANAGED_MARKER = '# helloagents-managed'
|
|
7
|
+
|
|
8
|
+
const HOOK_EVENT_KEY = {
|
|
9
|
+
PreToolUse: 'pre_tool_use',
|
|
10
|
+
PermissionRequest: 'permission_request',
|
|
11
|
+
PostToolUse: 'post_tool_use',
|
|
12
|
+
PreCompact: 'pre_compact',
|
|
13
|
+
PostCompact: 'post_compact',
|
|
14
|
+
SessionStart: 'session_start',
|
|
15
|
+
UserPromptSubmit: 'user_prompt_submit',
|
|
16
|
+
Stop: 'stop',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const EVENTS_WITH_MATCHER = new Set([
|
|
20
|
+
'PreToolUse',
|
|
21
|
+
'PermissionRequest',
|
|
22
|
+
'PostToolUse',
|
|
23
|
+
'PreCompact',
|
|
24
|
+
'PostCompact',
|
|
25
|
+
'SessionStart',
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
const HOOK_STATE_HEADER_RE = /^\[hooks\.state\."((?:\\.|[^"])*)"\](?:\s*#.*)?$/
|
|
29
|
+
|
|
30
|
+
function normalizeLineEndings(text = '') {
|
|
31
|
+
return String(text || '').replace(/\r\n/g, '\n')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function escapeTomlBasicString(value = '') {
|
|
35
|
+
return String(value || '')
|
|
36
|
+
.replace(/\\/g, '\\\\')
|
|
37
|
+
.replace(/"/g, '\\"')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function unescapeTomlBasicString(value = '') {
|
|
41
|
+
return String(value || '')
|
|
42
|
+
.replace(/\\"/g, '"')
|
|
43
|
+
.replace(/\\\\/g, '\\')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function canonicalizeJson(value) {
|
|
47
|
+
if (Array.isArray(value)) return value.map(canonicalizeJson)
|
|
48
|
+
if (!value || typeof value !== 'object') return value
|
|
49
|
+
|
|
50
|
+
return Object.keys(value)
|
|
51
|
+
.sort()
|
|
52
|
+
.reduce((acc, key) => {
|
|
53
|
+
if (value[key] !== undefined) acc[key] = canonicalizeJson(value[key])
|
|
54
|
+
return acc
|
|
55
|
+
}, {})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hashNormalizedHookIdentity(identity) {
|
|
59
|
+
const serialized = JSON.stringify(canonicalizeJson(identity))
|
|
60
|
+
return `sha256:${createHash('sha256').update(serialized).digest('hex')}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeHookMatcher(eventName, matcher) {
|
|
64
|
+
if (!EVENTS_WITH_MATCHER.has(eventName)) return undefined
|
|
65
|
+
return matcher === undefined ? undefined : String(matcher)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeHookTimeout(timeout) {
|
|
69
|
+
const value = Number(timeout)
|
|
70
|
+
if (!Number.isFinite(value)) return 600
|
|
71
|
+
return Math.max(1, Math.trunc(value))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildHookDescriptor(eventName, group, handler) {
|
|
75
|
+
return JSON.stringify({
|
|
76
|
+
eventName,
|
|
77
|
+
matcher: normalizeHookMatcher(eventName, group?.matcher),
|
|
78
|
+
command: handler?.command || '',
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildNormalizedHookIdentity(eventName, group, handler) {
|
|
83
|
+
const matcher = normalizeHookMatcher(eventName, group?.matcher)
|
|
84
|
+
const statusMessage = typeof handler?.statusMessage === 'string'
|
|
85
|
+
? handler.statusMessage
|
|
86
|
+
: undefined
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
event_name: HOOK_EVENT_KEY[eventName],
|
|
90
|
+
...(matcher !== undefined ? { matcher } : {}),
|
|
91
|
+
hooks: [
|
|
92
|
+
{
|
|
93
|
+
type: 'command',
|
|
94
|
+
command: String(handler?.command || ''),
|
|
95
|
+
timeout: normalizeHookTimeout(handler?.timeout),
|
|
96
|
+
async: Boolean(handler?.async),
|
|
97
|
+
...(statusMessage !== undefined ? { statusMessage } : {}),
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isHelloagentsCommandHandler(handler) {
|
|
104
|
+
return handler?.type === 'command'
|
|
105
|
+
&& typeof handler.command === 'string'
|
|
106
|
+
&& handler.command.includes('helloagents')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function serializeHookStateBlock(entry) {
|
|
110
|
+
const lines = [`[hooks.state."${escapeTomlBasicString(entry.key)}"]`]
|
|
111
|
+
if (entry.enabled === false) lines.push('enabled = false')
|
|
112
|
+
lines.push(`trusted_hash = "${escapeTomlBasicString(entry.trustedHash)}" ${MANAGED_MARKER}`)
|
|
113
|
+
return lines.join('\n')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function collectHookStateSections(text = '') {
|
|
117
|
+
const lines = normalizeLineEndings(text).split('\n')
|
|
118
|
+
const sections = []
|
|
119
|
+
|
|
120
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
121
|
+
const match = HOOK_STATE_HEADER_RE.exec(lines[index].trim())
|
|
122
|
+
if (!match) continue
|
|
123
|
+
|
|
124
|
+
let end = lines.length
|
|
125
|
+
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
126
|
+
if (isTomlTableHeader(lines[cursor])) {
|
|
127
|
+
end = cursor
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const bodyLines = lines.slice(index + 1, end)
|
|
133
|
+
const enabledLine = bodyLines.find((line) => /^\s*enabled\s*=/.test(line))
|
|
134
|
+
const trustedHashLine = bodyLines.find((line) => /^\s*trusted_hash\s*=/.test(line))
|
|
135
|
+
const trustedHashMatch = trustedHashLine?.match(/^\s*trusted_hash\s*=\s*"((?:\\.|[^"])*)"/)
|
|
136
|
+
|
|
137
|
+
sections.push({
|
|
138
|
+
key: unescapeTomlBasicString(match[1]),
|
|
139
|
+
start: index,
|
|
140
|
+
end,
|
|
141
|
+
enabled: /^\s*enabled\s*=\s*false\b/.test(enabledLine || '') ? false : undefined,
|
|
142
|
+
trustedHash: trustedHashMatch ? unescapeTomlBasicString(trustedHashMatch[1]) : '',
|
|
143
|
+
managed: lines[index].includes(MANAGED_MARKER)
|
|
144
|
+
|| bodyLines.some((line) => line.includes(MANAGED_MARKER)),
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
index = end - 1
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { lines, sections }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function removeHookStateSections(text, shouldRemove) {
|
|
154
|
+
const { lines, sections } = collectHookStateSections(text)
|
|
155
|
+
if (!sections.length) return normalizeToml(text)
|
|
156
|
+
|
|
157
|
+
const removedStarts = new Set(
|
|
158
|
+
sections
|
|
159
|
+
.filter(shouldRemove)
|
|
160
|
+
.map((section) => section.start),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if (!removedStarts.size) return normalizeToml(text)
|
|
164
|
+
|
|
165
|
+
const kept = []
|
|
166
|
+
for (let index = 0; index < lines.length;) {
|
|
167
|
+
const section = sections.find((item) => item.start === index)
|
|
168
|
+
if (!section) {
|
|
169
|
+
kept.push(lines[index])
|
|
170
|
+
index += 1
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
if (!removedStarts.has(section.start)) {
|
|
174
|
+
kept.push(...lines.slice(section.start, section.end))
|
|
175
|
+
}
|
|
176
|
+
index = section.end
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return normalizeToml(kept.join('\n'))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function appendHookStateBlocks(text, entries) {
|
|
183
|
+
if (!entries.length) return normalizeToml(text)
|
|
184
|
+
const blocks = entries.map(serializeHookStateBlock).join('\n\n')
|
|
185
|
+
const base = normalizeLineEndings(text).trimEnd()
|
|
186
|
+
return normalizeToml(base ? `${base}\n\n${blocks}` : blocks)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function buildManagedCodexHookTrustEntries(hooksPath, hooksData = safeJson(hooksPath)) {
|
|
190
|
+
const hooks = hooksData?.hooks
|
|
191
|
+
if (!hooks || typeof hooks !== 'object') return []
|
|
192
|
+
|
|
193
|
+
const entries = []
|
|
194
|
+
for (const eventName of Object.keys(HOOK_EVENT_KEY)) {
|
|
195
|
+
const groups = hooks[eventName]
|
|
196
|
+
if (!Array.isArray(groups)) continue
|
|
197
|
+
|
|
198
|
+
groups.forEach((group, groupIndex) => {
|
|
199
|
+
const handlers = Array.isArray(group?.hooks) ? group.hooks : []
|
|
200
|
+
handlers.forEach((handler, handlerIndex) => {
|
|
201
|
+
if (!isHelloagentsCommandHandler(handler)) return
|
|
202
|
+
|
|
203
|
+
const key = `${hooksPath}:${HOOK_EVENT_KEY[eventName]}:${groupIndex}:${handlerIndex}`
|
|
204
|
+
entries.push({
|
|
205
|
+
key,
|
|
206
|
+
trustedHash: hashNormalizedHookIdentity(
|
|
207
|
+
buildNormalizedHookIdentity(eventName, group, handler),
|
|
208
|
+
),
|
|
209
|
+
descriptor: buildHookDescriptor(eventName, group, handler),
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return entries
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function readCodexHookStateSections(text = '') {
|
|
219
|
+
return collectHookStateSections(text).sections
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function syncManagedCodexHookTrust(configPath, hooksPath, hooksData = safeJson(hooksPath)) {
|
|
223
|
+
const entries = buildManagedCodexHookTrustEntries(hooksPath, hooksData)
|
|
224
|
+
if (!entries.length) return cleanupManagedCodexHookTrust(configPath)
|
|
225
|
+
|
|
226
|
+
const keySet = new Set(entries.map((entry) => entry.key))
|
|
227
|
+
const existingText = safeRead(configPath) || ''
|
|
228
|
+
const existingSections = readCodexHookStateSections(existingText)
|
|
229
|
+
const enabledByDescriptor = new Map()
|
|
230
|
+
|
|
231
|
+
for (const section of existingSections) {
|
|
232
|
+
if (!keySet.has(section.key) || section.enabled !== false) continue
|
|
233
|
+
const matchingEntry = entries.find((entry) => entry.key === section.key)
|
|
234
|
+
if (matchingEntry) enabledByDescriptor.set(matchingEntry.descriptor, false)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const cleanedText = removeHookStateSections(
|
|
238
|
+
existingText,
|
|
239
|
+
(section) => section.managed || keySet.has(section.key),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
const nextEntries = entries.map((entry) => ({
|
|
243
|
+
...entry,
|
|
244
|
+
enabled: enabledByDescriptor.get(entry.descriptor),
|
|
245
|
+
}))
|
|
246
|
+
|
|
247
|
+
const nextText = appendHookStateBlocks(cleanedText, nextEntries)
|
|
248
|
+
if (normalizeLineEndings(nextText) === normalizeLineEndings(existingText)) return false
|
|
249
|
+
|
|
250
|
+
safeWrite(configPath, nextText)
|
|
251
|
+
return true
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function cleanupManagedCodexHookTrust(configPath) {
|
|
255
|
+
const existingText = safeRead(configPath)
|
|
256
|
+
if (!existingText) return false
|
|
257
|
+
|
|
258
|
+
const nextText = removeHookStateSections(existingText, (section) => section.managed)
|
|
259
|
+
if (normalizeLineEndings(nextText) === normalizeLineEndings(existingText)) return false
|
|
260
|
+
|
|
261
|
+
if (nextText.trim()) safeWrite(configPath, nextText)
|
|
262
|
+
else removeIfExists(configPath)
|
|
263
|
+
return true
|
|
264
|
+
}
|
package/scripts/cli-codex.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { join, dirname } from 'node:path';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import {
|
|
4
|
-
ensureDir, safeRead, safeWrite, removeIfExists,
|
|
5
|
-
readJsonOrThrow,
|
|
4
|
+
ensureDir, safeJson, safeRead, safeWrite, removeIfExists,
|
|
5
|
+
readJsonOrThrow,
|
|
6
6
|
createLink, removeLink, injectMarkedContent, removeMarkedContent,
|
|
7
7
|
cleanSettingsHooks, loadHooksWithCliEntry, mergeSettingsHooks,
|
|
8
8
|
} from './cli-utils.mjs';
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH,
|
|
13
13
|
CODEX_PLUGIN_CONFIG_HEADER,
|
|
14
14
|
installCodexManagedTopLevelConfig,
|
|
15
|
+
installCodexManagedTuiConfig,
|
|
15
16
|
isManagedCodexBackupInstruction,
|
|
16
17
|
isManagedCodexGoalsFeature,
|
|
17
18
|
isManagedCodexModelInstruction,
|
|
@@ -20,12 +21,17 @@ import {
|
|
|
20
21
|
readCodexGoalsFeatureLine,
|
|
21
22
|
readLegacyCodexHooksFeatureLine,
|
|
22
23
|
removeCodexGoalsFeatureConfig,
|
|
24
|
+
removeCodexManagedTuiConfig,
|
|
23
25
|
removeLegacyManagedCodexHooksFeatureConfig,
|
|
24
26
|
removeCodexPluginConfig,
|
|
25
27
|
restoreCodexGoalsFeatureConfig,
|
|
26
28
|
restoreCodexTopLevelConfig,
|
|
27
29
|
upsertCodexPluginConfig,
|
|
28
30
|
} from './cli-codex-config.mjs';
|
|
31
|
+
import {
|
|
32
|
+
cleanupManagedCodexHookTrust,
|
|
33
|
+
syncManagedCodexHookTrust,
|
|
34
|
+
} from './cli-codex-hooks-state.mjs';
|
|
29
35
|
import {
|
|
30
36
|
readTopLevelTomlLine,
|
|
31
37
|
readTopLevelTomlBlock,
|
|
@@ -132,11 +138,14 @@ function writeCodexRuntimeCarrier(filePath, bootstrapPath, settings) {
|
|
|
132
138
|
function installCodexStandaloneHooks(home, pkgRoot) {
|
|
133
139
|
const hooksData = loadHooksWithCliEntry(pkgRoot, 'hooks-codex.json', '${PLUGIN_ROOT}');
|
|
134
140
|
if (!hooksData) return false;
|
|
135
|
-
|
|
141
|
+
const hooksPath = join(home, '.codex', CODEX_HOOKS_BASENAME);
|
|
142
|
+
mergeSettingsHooks(hooksPath, hooksData);
|
|
143
|
+
syncManagedCodexHookTrust(join(home, '.codex', CODEX_CONFIG_BASENAME), hooksPath, safeJson(hooksPath));
|
|
136
144
|
return true;
|
|
137
145
|
}
|
|
138
146
|
|
|
139
147
|
function cleanupCodexStandaloneHooks(home) {
|
|
148
|
+
cleanupManagedCodexHookTrust(join(home, '.codex', CODEX_CONFIG_BASENAME));
|
|
140
149
|
cleanSettingsHooks(join(home, '.codex', CODEX_HOOKS_BASENAME));
|
|
141
150
|
}
|
|
142
151
|
|
|
@@ -160,6 +169,7 @@ function cleanupCodexManagedConfig(configPath, { removePluginConfig = false } =
|
|
|
160
169
|
if (shouldRestoreCodexGoalsFeature) {
|
|
161
170
|
toml = removeCodexGoalsFeatureConfig(toml);
|
|
162
171
|
}
|
|
172
|
+
toml = removeCodexManagedTuiConfig(toml);
|
|
163
173
|
if (shouldRemoveLegacyCodexHooksFeature) {
|
|
164
174
|
toml = removeLegacyManagedCodexHooksFeatureConfig(toml);
|
|
165
175
|
}
|
|
@@ -209,6 +219,7 @@ export function installCodexStandby(home, pkgRoot) {
|
|
|
209
219
|
toml = installCodexManagedTopLevelConfig(toml, {
|
|
210
220
|
modelInstructionsPath: CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH,
|
|
211
221
|
});
|
|
222
|
+
toml = installCodexManagedTuiConfig(toml);
|
|
212
223
|
toml = removeLegacyManagedCodexHooksFeatureConfig(toml);
|
|
213
224
|
safeWrite(configPath, toml);
|
|
214
225
|
installCodexStandaloneHooks(home, pkgRoot);
|
|
@@ -277,20 +288,15 @@ export function installCodexGlobal(home, pkgRoot) {
|
|
|
277
288
|
removeIfExists(join(codexDir, 'plugins', 'cache', CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME));
|
|
278
289
|
|
|
279
290
|
ensureDir(join(home, 'plugins'));
|
|
280
|
-
ensureDir(installedPluginRoot);
|
|
291
|
+
ensureDir(dirname(installedPluginRoot));
|
|
281
292
|
|
|
282
293
|
const settings = readCarrierSettings(home);
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
createLink(
|
|
286
|
-
writeCodexRuntimeCarrier(
|
|
287
|
-
join(pluginRoot, CODEX_RUNTIME_CARRIER),
|
|
288
|
-
join(pluginRoot, 'bootstrap.md'),
|
|
289
|
-
settings,
|
|
290
|
-
);
|
|
294
|
+
createLink(pkgRoot, pluginRoot);
|
|
295
|
+
createLink(pkgRoot, installedPluginRoot);
|
|
296
|
+
createLink(pkgRoot, join(codexDir, 'helloagents'));
|
|
291
297
|
writeCodexRuntimeCarrier(
|
|
292
|
-
join(
|
|
293
|
-
join(
|
|
298
|
+
join(pkgRoot, CODEX_RUNTIME_CARRIER),
|
|
299
|
+
join(pkgRoot, 'bootstrap.md'),
|
|
294
300
|
settings,
|
|
295
301
|
);
|
|
296
302
|
const homeCarrierPath = join(codexDir, CODEX_RUNTIME_CARRIER);
|
|
@@ -304,6 +310,7 @@ export function installCodexGlobal(home, pkgRoot) {
|
|
|
304
310
|
toml = installCodexManagedTopLevelConfig(toml, {
|
|
305
311
|
modelInstructionsPath: CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH,
|
|
306
312
|
});
|
|
313
|
+
toml = installCodexManagedTuiConfig(toml);
|
|
307
314
|
toml = removeLegacyManagedCodexHooksFeatureConfig(toml);
|
|
308
315
|
toml = upsertCodexPluginConfig(toml);
|
|
309
316
|
safeWrite(configPath, toml);
|
|
@@ -9,6 +9,11 @@ import {
|
|
|
9
9
|
readCodexHooksFeatureLine,
|
|
10
10
|
readLegacyCodexHooksFeatureLine,
|
|
11
11
|
} from './cli-codex-config.mjs'
|
|
12
|
+
import {
|
|
13
|
+
buildManagedCodexHookTrustEntries,
|
|
14
|
+
readCodexHookStateSections,
|
|
15
|
+
} from './cli-codex-hooks-state.mjs'
|
|
16
|
+
import { getStableRuntimeRoot } from './cli-runtime-root.mjs'
|
|
12
17
|
import { buildRuntimeCarrier } from './cli-runtime-carrier.mjs'
|
|
13
18
|
import { readTopLevelTomlLine } from './cli-toml.mjs'
|
|
14
19
|
import { loadHooksWithCliEntry, safeJson, safeRead } from './cli-utils.mjs'
|
|
@@ -101,6 +106,8 @@ function appendCodexStandbyIssues(runtime, issues, checks) {
|
|
|
101
106
|
if (!checks.codexHooksFeature) issues.push(buildDoctorIssue(runtime, 'codex-hooks-feature-disabled', 'Codex hooks 功能被显式关闭', 'Codex hooks feature is explicitly disabled'))
|
|
102
107
|
if (!checks.standaloneHooks) issues.push(buildDoctorIssue(runtime, 'standby-hooks-missing', 'standby `~/.codex/hooks.json` 缺少 HelloAGENTS hooks', 'Standby `~/.codex/hooks.json` is missing HelloAGENTS hooks'))
|
|
103
108
|
if (checks.standaloneHooks && !checks.standaloneHooksMatch) issues.push(buildDoctorIssue(runtime, 'standby-hooks-drift', 'standby `~/.codex/hooks.json` 与当前 hooks-codex.json 不一致', 'Standby `~/.codex/hooks.json` differs from the current hooks-codex.json'))
|
|
109
|
+
if (checks.standaloneHooks && !checks.managedHookTrust) issues.push(buildDoctorIssue(runtime, 'standby-hook-trust-missing', 'standby `config.toml` 缺少 HelloAGENTS hooks trust', 'Standby `config.toml` is missing HelloAGENTS hook trust metadata'))
|
|
110
|
+
if (checks.standaloneHooks && checks.managedHookTrust && !checks.managedHookTrustMatch) issues.push(buildDoctorIssue(runtime, 'standby-hook-trust-drift', 'standby hooks trust 与当前 hooks 定义不一致', 'Standby hook trust metadata differs from the current hooks definition'))
|
|
104
111
|
if (checks.pluginRoot || checks.pluginCache || checks.marketplaceEntry || checks.pluginEnabled) {
|
|
105
112
|
issues.push(buildDoctorIssue(runtime, 'standby-global-residue', 'standby 模式下仍残留 global 插件文件或配置', 'Global plugin artifacts still remain while Codex is in standby mode'))
|
|
106
113
|
}
|
|
@@ -109,9 +116,11 @@ function appendCodexStandbyIssues(runtime, issues, checks) {
|
|
|
109
116
|
function appendCodexGlobalIssues(runtime, issues, checks, pluginVersion, cacheVersion) {
|
|
110
117
|
if (!checks.carrierMarker) issues.push(buildDoctorIssue(runtime, 'global-home-carrier-missing', 'global `~/.codex/AGENTS.md` 缺少 HelloAGENTS 规则内容', 'Global `~/.codex/AGENTS.md` is missing the HelloAGENTS carrier'))
|
|
111
118
|
if (checks.carrierMarker && !checks.carrierContentMatch) issues.push(buildDoctorIssue(runtime, 'global-home-carrier-drift', 'global `~/.codex/AGENTS.md` 与当前全局模式规则不一致', 'Global `~/.codex/AGENTS.md` differs from the current global rules'))
|
|
112
|
-
if (!checks.globalHomeLink) issues.push(buildDoctorIssue(runtime, 'global-read-root-link-missing', 'global `~/.codex/helloagents`
|
|
119
|
+
if (!checks.globalHomeLink) issues.push(buildDoctorIssue(runtime, 'global-read-root-link-missing', 'global `~/.codex/helloagents` 链接缺失或未指向稳定运行根目录', 'Global `~/.codex/helloagents` link is missing or does not point to the stable runtime root'))
|
|
113
120
|
if (!checks.pluginRoot) issues.push(buildDoctorIssue(runtime, 'global-plugin-root-missing', 'global 插件根目录缺失', 'Global plugin root is missing'))
|
|
114
121
|
if (!checks.pluginCache) issues.push(buildDoctorIssue(runtime, 'global-plugin-cache-missing', 'global 插件缓存目录缺失', 'Global plugin cache directory is missing'))
|
|
122
|
+
if (checks.pluginRoot && !checks.pluginRootLink) issues.push(buildDoctorIssue(runtime, 'global-plugin-root-link-drift', 'global 插件根目录未链接到稳定运行根目录', 'Global plugin root does not link to the stable runtime root'))
|
|
123
|
+
if (checks.pluginCache && !checks.pluginCacheLink) issues.push(buildDoctorIssue(runtime, 'global-plugin-cache-link-drift', 'global 插件缓存未链接到稳定运行根目录', 'Global plugin cache does not link to the stable runtime root'))
|
|
115
124
|
if (checks.pluginRoot && !checks.pluginCarrierMatch) issues.push(buildDoctorIssue(runtime, 'global-plugin-carrier-drift', 'global 插件根目录中的 AGENTS.md 与当前全局模式规则不一致', 'Global plugin AGENTS.md differs from the current global rules'))
|
|
116
125
|
if (checks.pluginCache && !checks.pluginCacheCarrierMatch) issues.push(buildDoctorIssue(runtime, 'global-plugin-cache-carrier-drift', 'global 插件缓存中的 AGENTS.md 与当前全局模式规则不一致', 'Global plugin cache AGENTS.md differs from the current global rules'))
|
|
117
126
|
if (!checks.marketplaceEntry) issues.push(buildDoctorIssue(runtime, 'global-marketplace-missing', 'global marketplace 条目缺失', 'Global marketplace entry is missing'))
|
|
@@ -123,9 +132,10 @@ function appendCodexGlobalIssues(runtime, issues, checks, pluginVersion, cacheVe
|
|
|
123
132
|
if (!checks.codexHooksFeature) issues.push(buildDoctorIssue(runtime, 'codex-hooks-feature-disabled', 'Codex hooks 功能被显式关闭', 'Codex hooks feature is explicitly disabled'))
|
|
124
133
|
if (!checks.standaloneHooks) issues.push(buildDoctorIssue(runtime, 'global-hooks-missing', 'global `~/.codex/hooks.json` 缺少 HelloAGENTS hooks', 'Global `~/.codex/hooks.json` is missing HelloAGENTS hooks'))
|
|
125
134
|
if (checks.standaloneHooks && !checks.standaloneHooksMatch) issues.push(buildDoctorIssue(runtime, 'global-hooks-drift', 'global `~/.codex/hooks.json` 与当前 hooks-codex.json 不一致', 'Global `~/.codex/hooks.json` differs from the current hooks-codex.json'))
|
|
135
|
+
if (checks.standaloneHooks && !checks.managedHookTrust) issues.push(buildDoctorIssue(runtime, 'global-hook-trust-missing', 'global `config.toml` 缺少 HelloAGENTS hooks trust', 'Global `config.toml` is missing HelloAGENTS hook trust metadata'))
|
|
136
|
+
if (checks.standaloneHooks && checks.managedHookTrust && !checks.managedHookTrustMatch) issues.push(buildDoctorIssue(runtime, 'global-hook-trust-drift', 'global hooks trust 与当前 hooks 定义不一致', 'Global hook trust metadata differs from the current hooks definition'))
|
|
126
137
|
if (pluginVersion && !checks.pluginVersionMatch) issues.push(buildDoctorIssue(runtime, 'global-plugin-version-drift', 'global 插件根目录版本与当前包版本不一致', 'Global plugin root version does not match the current package version'))
|
|
127
138
|
if (cacheVersion && !checks.pluginCacheVersionMatch) issues.push(buildDoctorIssue(runtime, 'global-plugin-cache-version-drift', 'global 插件缓存版本与当前包版本不一致', 'Global plugin cache version does not match the current package version'))
|
|
128
|
-
if (checks.homeLink) issues.push(buildDoctorIssue(runtime, 'global-standby-link-residue', 'global 模式下仍残留 standby home 链接', 'Standby home link still remains while Codex is in global mode'))
|
|
129
139
|
}
|
|
130
140
|
|
|
131
141
|
function buildCodexChecks(runtime, settings, trackedMode, detectedMode) {
|
|
@@ -133,7 +143,10 @@ function buildCodexChecks(runtime, settings, trackedMode, detectedMode) {
|
|
|
133
143
|
const codexConfig = safeRead(join(codexDir, 'config.toml')) || ''
|
|
134
144
|
const pluginRoot = join(runtime.home, 'plugins', CODEX_PLUGIN_NAME)
|
|
135
145
|
const pluginCacheRoot = join(codexDir, 'plugins', 'cache', CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME, 'local')
|
|
146
|
+
const runtimeRoot = safeRealTarget(getStableRuntimeRoot(runtime.home)) || normalizePath(getStableRuntimeRoot(runtime.home))
|
|
136
147
|
const homeLinkTarget = safeRealTarget(join(codexDir, 'helloagents'))
|
|
148
|
+
const pluginRootTarget = safeRealTarget(pluginRoot)
|
|
149
|
+
const pluginCacheTarget = safeRealTarget(pluginCacheRoot)
|
|
137
150
|
const expectedHomeCarrier = (detectedMode === 'global' || (detectedMode === 'none' && trackedMode === 'global'))
|
|
138
151
|
? 'bootstrap.md'
|
|
139
152
|
: 'bootstrap-lite.md'
|
|
@@ -141,6 +154,12 @@ function buildCodexChecks(runtime, settings, trackedMode, detectedMode) {
|
|
|
141
154
|
const marketplace = safeJson(join(runtime.home, '.agents', 'plugins', 'marketplace.json')) || {}
|
|
142
155
|
const modelInstructionsLine = readTopLevelTomlLine(codexConfig, 'model_instructions_file')
|
|
143
156
|
const expectedHooks = readExpectedHooks(runtime, 'hooks-codex.json', '${PLUGIN_ROOT}')
|
|
157
|
+
const expectedHookTrust = buildManagedCodexHookTrustEntries(join(codexDir, 'hooks.json'), codexHooks)
|
|
158
|
+
const managedHookTrust = new Map(
|
|
159
|
+
readCodexHookStateSections(codexConfig)
|
|
160
|
+
.filter((section) => section.managed)
|
|
161
|
+
.map((section) => [section.key, section.trustedHash]),
|
|
162
|
+
)
|
|
144
163
|
const hooksFeatureLine = readCodexHooksFeatureLine(codexConfig)
|
|
145
164
|
const goalsFeatureLine = readCodexGoalsFeatureLine(codexConfig)
|
|
146
165
|
const legacyHooksFeatureLine = readLegacyCodexHooksFeatureLine(codexConfig)
|
|
@@ -151,7 +170,7 @@ function buildCodexChecks(runtime, settings, trackedMode, detectedMode) {
|
|
|
151
170
|
carrierContentMatch: normalizeText((safeRead(join(codexDir, 'AGENTS.md')) || '').match(/<!-- HELLOAGENTS_START -->([\s\S]*?)<!-- HELLOAGENTS_END -->/)?.[1] || '')
|
|
152
171
|
=== readExpectedCarrierContent(runtime, expectedHomeCarrier, settings),
|
|
153
172
|
homeLink: homeLinkTarget === (safeRealTarget(runtime.pkgRoot) || normalizePath(runtime.pkgRoot)),
|
|
154
|
-
globalHomeLink: homeLinkTarget ===
|
|
173
|
+
globalHomeLink: homeLinkTarget === runtimeRoot,
|
|
155
174
|
modelInstructionsFile: !!modelInstructionsLine,
|
|
156
175
|
modelInstructionsPathMatch: !!modelInstructionsLine && normalizePath(modelInstructionsLine).includes(`"${CODEX_MANAGED_MODEL_INSTRUCTIONS_PATH}"`),
|
|
157
176
|
codexNotify: codexConfig.includes('codex-notify'),
|
|
@@ -162,8 +181,12 @@ function buildCodexChecks(runtime, settings, trackedMode, detectedMode) {
|
|
|
162
181
|
legacyCodexHooksFeature: Boolean(legacyHooksFeatureLine),
|
|
163
182
|
standaloneHooks: JSON.stringify(codexHooks.hooks || {}).includes('helloagents'),
|
|
164
183
|
standaloneHooksMatch: managedHooksMatch(codexHooks.hooks || {}, expectedHooks),
|
|
184
|
+
managedHookTrust: expectedHookTrust.every((entry) => managedHookTrust.has(entry.key)),
|
|
185
|
+
managedHookTrustMatch: expectedHookTrust.every((entry) => managedHookTrust.get(entry.key) === entry.trustedHash),
|
|
165
186
|
pluginRoot: existsSync(pluginRoot),
|
|
166
187
|
pluginCache: existsSync(pluginCacheRoot),
|
|
188
|
+
pluginRootLink: pluginRootTarget === runtimeRoot,
|
|
189
|
+
pluginCacheLink: pluginCacheTarget === runtimeRoot,
|
|
167
190
|
pluginCarrierMatch: normalizeText(safeRead(join(pluginRoot, 'AGENTS.md')) || '') === readExpectedCarrierContent(runtime, 'bootstrap.md', settings),
|
|
168
191
|
pluginCacheCarrierMatch: normalizeText(safeRead(join(pluginCacheRoot, 'AGENTS.md')) || '') === readExpectedCarrierContent(runtime, 'bootstrap.md', settings),
|
|
169
192
|
marketplaceEntry: Array.isArray(marketplace.plugins) && marketplace.plugins.some((plugin) => plugin?.name === CODEX_PLUGIN_NAME),
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
CODEX_PLUGIN_KEY,
|
|
7
7
|
CODEX_PLUGIN_NAME,
|
|
8
8
|
} from './cli-codex.mjs'
|
|
9
|
+
import { getStableRuntimeRoot } from './cli-runtime-root.mjs'
|
|
9
10
|
import { safeJson, safeRead } from './cli-utils.mjs'
|
|
10
11
|
|
|
11
12
|
const HOST_ALIASES = new Map([
|
|
@@ -68,19 +69,18 @@ function detectCodexMode(home) {
|
|
|
68
69
|
const codexHomeLink = join(codexDir, 'helloagents')
|
|
69
70
|
const codexConfig = safeRead(join(codexDir, 'config.toml')) || ''
|
|
70
71
|
const marketplace = safeRead(join(home, '.agents', 'plugins', 'marketplace.json')) || ''
|
|
71
|
-
const
|
|
72
|
+
const runtimeRoot = normalizePath(getStableRuntimeRoot(home))
|
|
72
73
|
const codexHomeLinkTarget = safeRealTarget(codexHomeLink)
|
|
73
74
|
if (
|
|
74
75
|
existsSync(join(home, 'plugins', CODEX_PLUGIN_NAME))
|
|
75
76
|
|| existsSync(join(codexDir, 'plugins', 'cache', CODEX_MARKETPLACE_NAME, CODEX_PLUGIN_NAME))
|
|
76
77
|
|| marketplace.includes(`"name": "${CODEX_PLUGIN_NAME}"`)
|
|
77
78
|
|| codexConfig.includes(CODEX_PLUGIN_KEY)
|
|
78
|
-
|| codexHomeLinkTarget === globalPluginRoot
|
|
79
79
|
) {
|
|
80
80
|
return 'global'
|
|
81
81
|
}
|
|
82
82
|
if (
|
|
83
|
-
(existsSync(codexHomeLink) && codexHomeLinkTarget
|
|
83
|
+
(existsSync(codexHomeLink) && codexHomeLinkTarget === runtimeRoot)
|
|
84
84
|
|| hasHelloagentsMarker(join(codexDir, 'AGENTS.md'))
|
|
85
85
|
|| codexConfig.includes('codex-notify')
|
|
86
86
|
|| codexConfig.includes('HelloAGENTS')
|
|
@@ -59,14 +59,18 @@ function clearTrackedHostMode(settings, host) {
|
|
|
59
59
|
delete settings.host_install_modes[host]
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
function setAllTrackedHostModes(settings, mode) {
|
|
63
|
-
settings.host_install_modes = Object.fromEntries(HOSTS.map((host) => [host, mode]))
|
|
64
|
-
}
|
|
65
|
-
|
|
66
62
|
function clearAllTrackedHostModes(settings) {
|
|
67
63
|
settings.host_install_modes = {}
|
|
68
64
|
}
|
|
69
65
|
|
|
66
|
+
function syncTrackedHostMode(settings, host, result, mode) {
|
|
67
|
+
if (!result?.skipped && result?.ok !== false) {
|
|
68
|
+
setTrackedHostMode(settings, host, mode)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
clearTrackedHostMode(settings, host)
|
|
72
|
+
}
|
|
73
|
+
|
|
70
74
|
export function normalizeHost(value = '') {
|
|
71
75
|
return normalizeLifecycleHost(value)
|
|
72
76
|
}
|
|
@@ -157,8 +161,11 @@ export function switchMode(newMode) {
|
|
|
157
161
|
runtime.ok(runtime.msg(`当前已是 ${newMode} 模式,正在刷新安装`, `Already in ${newMode} mode, refreshing installation`))
|
|
158
162
|
}
|
|
159
163
|
|
|
160
|
-
installAllHosts(runtime, newMode)
|
|
161
|
-
|
|
164
|
+
const results = installAllHosts(runtime, newMode)
|
|
165
|
+
clearAllTrackedHostModes(config)
|
|
166
|
+
for (const host of HOSTS) {
|
|
167
|
+
syncTrackedHostMode(config, host, results?.[host], newMode)
|
|
168
|
+
}
|
|
162
169
|
writeSettings(config)
|
|
163
170
|
runtime.printInstallMsg(newMode, isRefresh ? 'refresh' : 'switch')
|
|
164
171
|
}
|
|
@@ -229,7 +236,7 @@ export function runScopedLifecycle(action, rawArgs) {
|
|
|
229
236
|
writeSettings(settings)
|
|
230
237
|
}
|
|
231
238
|
} else if (!result.skipped) {
|
|
232
|
-
|
|
239
|
+
syncTrackedHostMode(settings, host, result, mode)
|
|
233
240
|
writeSettings(settings)
|
|
234
241
|
}
|
|
235
242
|
}
|
package/scripts/cli-messages.mjs
CHANGED
|
@@ -87,7 +87,7 @@ HelloAGENTS v${pkgVersion} — The orchestration kernel for AI CLIs
|
|
|
87
87
|
${msg('安装', 'Install')}:
|
|
88
88
|
npm install -g helloagents ${msg('(安装命令并同步稳定运行根目录;CLI 部署需显式执行 helloagents install ...)', '(installs the command and syncs the stable runtime root; deploy to CLIs explicitly with helloagents install ...)')}
|
|
89
89
|
HELLOAGENTS=codex:global npm install -g helloagents
|
|
90
|
-
helloagents-js
|
|
90
|
+
helloagents-js ${msg('(受管宿主配置的跨平台稳定入口)', '(cross-platform stable entrypoint for managed host configs)')}
|
|
91
91
|
|
|
92
92
|
${msg('模式切换', 'Mode switching')}:
|
|
93
93
|
helloagents --global ${msg('全局模式(自动尝试 Claude/Gemini 插件或扩展;Codex 自动装原生本地插件)', 'Global mode (auto-attempts Claude/Gemini plugins or extensions; native local plugin auto-install for Codex)')}
|
package/scripts/cli-utils.mjs
CHANGED
|
@@ -35,6 +35,7 @@ export function copyEntries(sourceRoot, targetRoot, entries) {
|
|
|
35
35
|
export function createLink(target, linkPath) {
|
|
36
36
|
removeLink(linkPath);
|
|
37
37
|
try {
|
|
38
|
+
ensureDir(dirname(linkPath));
|
|
38
39
|
symlinkSync(target, linkPath, IS_WIN ? 'junction' : 'dir');
|
|
39
40
|
return true;
|
|
40
41
|
} catch { return false; }
|
|
@@ -132,9 +133,9 @@ export function cleanSettingsHooks(settingsPath, cleanPermissions = false) {
|
|
|
132
133
|
|
|
133
134
|
function rewriteHookCommandToCli(command = '', pathVar = '') {
|
|
134
135
|
const replacements = new Map([
|
|
135
|
-
[`node "${pathVar}/scripts/notify.mjs"`, 'helloagents-js
|
|
136
|
-
[`node "${pathVar}/scripts/guard.mjs"`, 'helloagents-js
|
|
137
|
-
[`node "${pathVar}/scripts/ralph-loop.mjs"`, 'helloagents-js
|
|
136
|
+
[`node "${pathVar}/scripts/notify.mjs"`, 'helloagents-js notify'],
|
|
137
|
+
[`node "${pathVar}/scripts/guard.mjs"`, 'helloagents-js guard'],
|
|
138
|
+
[`node "${pathVar}/scripts/ralph-loop.mjs"`, 'helloagents-js ralph-loop'],
|
|
138
139
|
]);
|
|
139
140
|
|
|
140
141
|
let next = command;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* or when the plan artifacts are incomplete enough that delivery is not trustworthy.
|
|
6
6
|
*/
|
|
7
7
|
import { readFileSync } from 'node:fs'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
8
9
|
import { getAdvisorEvidenceStatus } from './advisor-state.mjs'
|
|
9
10
|
import { getCloseoutEvidenceStatus } from './closeout-state.mjs'
|
|
10
11
|
import { getAdvisorRequirement, getVisualValidationRequirement } from './plan-contract.mjs'
|
|
@@ -129,11 +130,15 @@ function collectGateIssues(planEntries, verificationStatus, reviewStatus, adviso
|
|
|
129
130
|
return issues
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
function
|
|
133
|
-
let data = {}
|
|
133
|
+
function readStdinJson() {
|
|
134
134
|
try {
|
|
135
|
-
|
|
136
|
-
} catch {
|
|
135
|
+
return JSON.parse(readFileSync(0, 'utf-8'))
|
|
136
|
+
} catch {
|
|
137
|
+
return {}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function evaluateDeliveryGate(data = {}) {
|
|
137
142
|
const cwd = data.cwd || process.cwd()
|
|
138
143
|
const workflowOptions = { payload: data }
|
|
139
144
|
const snapshot = getWorkflowSnapshot(cwd, workflowOptions)
|
|
@@ -146,8 +151,7 @@ function main() {
|
|
|
146
151
|
...workflowOptions,
|
|
147
152
|
})
|
|
148
153
|
if (gatePlans.length === 0) {
|
|
149
|
-
|
|
150
|
-
return
|
|
154
|
+
return { suppressOutput: true }
|
|
151
155
|
}
|
|
152
156
|
|
|
153
157
|
const advisorRequirements = gatePlans.map((entry) => getAdvisorRequirement(entry.contract))
|
|
@@ -177,15 +181,20 @@ function main() {
|
|
|
177
181
|
|
|
178
182
|
const issues = collectGateIssues(gatePlans, verificationStatus, reviewStatus, advisorStatus, visualStatus, closeoutStatus)
|
|
179
183
|
if (issues.length === 0) {
|
|
180
|
-
|
|
181
|
-
return
|
|
184
|
+
return { suppressOutput: true }
|
|
182
185
|
}
|
|
183
186
|
|
|
184
|
-
|
|
187
|
+
return {
|
|
185
188
|
decision: 'block',
|
|
186
189
|
reason: buildDeliveryBlockReason(issues, recommendation, buildDeliveryGateHint(cwd, workflowOptions)),
|
|
187
190
|
suppressOutput: true,
|
|
188
|
-
}
|
|
191
|
+
}
|
|
189
192
|
}
|
|
190
193
|
|
|
191
|
-
main()
|
|
194
|
+
function main() {
|
|
195
|
+
process.stdout.write(JSON.stringify(evaluateDeliveryGate(readStdinJson())))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
199
|
+
main()
|
|
200
|
+
}
|