helloagents 3.0.23 → 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.
@@ -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, copyEntries,
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
- mergeSettingsHooks(join(home, '.codex', CODEX_HOOKS_BASENAME), hooksData);
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
- copyEntries(pkgRoot, pluginRoot, CODEX_RUNTIME_ENTRIES);
284
- copyEntries(pkgRoot, installedPluginRoot, CODEX_RUNTIME_ENTRIES);
285
- createLink(pluginRoot, join(codexDir, 'helloagents'));
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(installedPluginRoot, CODEX_RUNTIME_CARRIER),
293
- join(installedPluginRoot, 'bootstrap.md'),
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` 链接缺失或未指向当前插件根目录', 'Global `~/.codex/helloagents` link is missing or does not point to the current plugin root'))
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 === (safeRealTarget(pluginRoot) || normalizePath(pluginRoot)),
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 globalPluginRoot = normalizePath(join(home, 'plugins', CODEX_PLUGIN_NAME))
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 !== globalPluginRoot)
83
+ (existsSync(codexHomeLink) && codexHomeLinkTarget === runtimeRoot)
84
84
  || hasHelloagentsMarker(join(codexDir, 'AGENTS.md'))
85
85
  || codexConfig.includes('codex-notify')
86
86
  || codexConfig.includes('HelloAGENTS')
@@ -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.cmd ${msg('(受管宿主配置的跨平台稳定入口)', '(cross-platform stable entrypoint for managed host configs)')}
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)')}
@@ -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.cmd notify'],
136
- [`node "${pathVar}/scripts/guard.mjs"`, 'helloagents-js.cmd guard'],
137
- [`node "${pathVar}/scripts/ralph-loop.mjs"`, 'helloagents-js.cmd ralph-loop'],
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 main() {
133
- let data = {}
133
+ function readStdinJson() {
134
134
  try {
135
- data = JSON.parse(readFileSync(0, 'utf-8'))
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
- process.stdout.write(JSON.stringify({ suppressOutput: true }))
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
- process.stdout.write(JSON.stringify({ suppressOutput: true }))
181
- return
184
+ return { suppressOutput: true }
182
185
  }
183
186
 
184
- process.stdout.write(JSON.stringify({
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
+ }
@@ -5,6 +5,7 @@ import { dirname } from 'node:path'
5
5
  import { getRuntimeEvidencePath, readRuntimeEvidence, writeRuntimeEvidence } from './runtime-artifacts.mjs'
6
6
 
7
7
  export const CODEX_CLOSEOUT_EVIDENCE_FILE = 'codex-native-stop.json'
8
+ export const CODEX_QUICK_NOTIFY_EVIDENCE_FILE = 'codex-quick-notify.json'
8
9
  const CODEX_CLOSEOUT_LOCK_FILE = 'codex-native-stop.lock'
9
10
  const WEAK_KEY_TTL_MS = 10_000
10
11
  const LOCK_STALE_MS = 120_000
@@ -110,8 +111,7 @@ export function matchesCodexCloseoutEvidence(evidence, snapshot, now = Date.now(
110
111
  if (intersects(snapshot.strongKeys, strongKeys)) return true
111
112
 
112
113
  const currentHasStrong = snapshot.strongKeys.length > 0
113
- const evidenceHasStrong = strongKeys.length > 0
114
- if (currentHasStrong && evidenceHasStrong) return false
114
+ if (currentHasStrong) return false
115
115
 
116
116
  const updatedAt = Date.parse(evidence.updatedAt || '')
117
117
  if (!Number.isFinite(updatedAt) || now - updatedAt > WEAK_KEY_TTL_MS) return false
@@ -211,3 +211,23 @@ export function finalizeCodexCloseoutClaim(claim, meta = {}) {
211
211
  releaseLockFile(claim.lockPath)
212
212
  }
213
213
  }
214
+
215
+ export function writeCodexQuickNotifyEvidence(cwd, { payload = {}, turnState = null, event = '' } = {}) {
216
+ const snapshot = buildCodexCloseoutSnapshot({ payload, turnState })
217
+ return writeRuntimeEvidence(cwd, CODEX_QUICK_NOTIFY_EVIDENCE_FILE, {
218
+ version: 1,
219
+ updatedAt: new Date().toISOString(),
220
+ event,
221
+ turnId: snapshot.turnId,
222
+ sessionId: snapshot.sessionId,
223
+ messageHash: snapshot.messageHash,
224
+ strongKeys: snapshot.strongKeys,
225
+ weakKeys: snapshot.weakKeys,
226
+ }, { payload })
227
+ }
228
+
229
+ export function hasCodexQuickNotifyEvidence(cwd, { payload = {}, turnState = null } = {}) {
230
+ const snapshot = buildCodexCloseoutSnapshot({ payload, turnState })
231
+ const evidence = readRuntimeEvidence(cwd, CODEX_QUICK_NOTIFY_EVIDENCE_FILE, { payload })
232
+ return matchesCodexCloseoutEvidence(evidence, snapshot)
233
+ }
@@ -38,6 +38,7 @@ function routeExplicitCommand({
38
38
  appendReplayEvent,
39
39
  buildRouteInstruction,
40
40
  suppress,
41
+ recordReplayEvents,
41
42
  }) {
42
43
  const cmdMatch = prompt.match(/^~(\w+)/)
43
44
  if (!cmdMatch) return false
@@ -50,14 +51,16 @@ function routeExplicitCommand({
50
51
  sourceSkillName: skillName,
51
52
  payload,
52
53
  })
53
- appendReplayEvent(cwd, {
54
- host,
55
- event: 'command_route_selected',
56
- source: 'route',
57
- skillName: canonicalSkillName,
58
- sourceSkillName: skillName,
59
- payload,
60
- })
54
+ if (recordReplayEvents !== false) {
55
+ appendReplayEvent(cwd, {
56
+ host,
57
+ event: 'command_route_selected',
58
+ source: 'route',
59
+ skillName: canonicalSkillName,
60
+ sourceSkillName: skillName,
61
+ payload,
62
+ })
63
+ }
61
64
  suppress(buildRouteInstruction({
62
65
  skillName,
63
66
  extraRules: buildHelpExtraRules(skillName),
@@ -84,6 +87,7 @@ export function handleRouteCommand({
84
87
  getWorkflowRecommendation,
85
88
  suppress,
86
89
  emptySuppress,
90
+ recordReplayEvents = true,
87
91
  }) {
88
92
  const prompt = (payload.prompt || '').trim()
89
93
  const cwd = payload.cwd || process.cwd()
@@ -105,6 +109,7 @@ export function handleRouteCommand({
105
109
  appendReplayEvent,
106
110
  buildRouteInstruction,
107
111
  suppress,
112
+ recordReplayEvents,
108
113
  })) {
109
114
  return
110
115
  }
@@ -112,13 +117,15 @@ export function handleRouteCommand({
112
117
  const bootstrapFile = resolveBootstrapFile(cwd, settings, host)
113
118
  if (bootstrapFile === 'bootstrap.md') {
114
119
  clearRouteContext({ cwd, payload })
115
- appendReplayEvent(cwd, {
116
- host,
117
- event: 'semantic_route_prompted',
118
- source: 'route',
119
- recommendation: getWorkflowRecommendation(cwd, { payload }),
120
- payload,
121
- })
120
+ if (recordReplayEvents !== false) {
121
+ appendReplayEvent(cwd, {
122
+ host,
123
+ event: 'semantic_route_prompted',
124
+ source: 'route',
125
+ recommendation: getWorkflowRecommendation(cwd, { payload }),
126
+ payload,
127
+ })
128
+ }
122
129
  suppress(buildSemanticRouteInstruction(cwd, payload))
123
130
  return
124
131
  }
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process'
3
+ import { existsSync } from 'node:fs'
4
+ import { platform } from 'node:os'
5
+ import { join, dirname } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ const __filename = fileURLToPath(import.meta.url)
9
+ const __dirname = dirname(__filename)
10
+ const PKG_ROOT = join(__dirname, '..')
11
+ const PLAT = platform()
12
+
13
+ function shellQuote(value = '') {
14
+ return `'${String(value).replace(/'/g, `'\\''`)}'`
15
+ }
16
+
17
+ export function resolveSoundPath(event = '', pkgRoot = PKG_ROOT) {
18
+ const filePath = join(pkgRoot, 'assets', 'sounds', `${event}.wav`)
19
+ return existsSync(filePath) ? filePath : ''
20
+ }
21
+
22
+ export function buildWindowsSoundCommand(filePath = '') {
23
+ return `(New-Object System.Media.SoundPlayer '${String(filePath || '').replace(/'/g, "''")}').PlaySync()`
24
+ }
25
+
26
+ function playWindows(filePath) {
27
+ const result = spawnSync('powershell', [
28
+ '-NoProfile',
29
+ '-c',
30
+ buildWindowsSoundCommand(filePath),
31
+ ], {
32
+ encoding: 'utf-8',
33
+ windowsHide: true,
34
+ })
35
+
36
+ if (result.error) throw result.error
37
+ if (result.status !== 0) {
38
+ throw new Error((result.stderr || result.stdout || 'PowerShell sound playback failed').trim())
39
+ }
40
+ }
41
+
42
+ function playMac(filePath) {
43
+ const result = spawnSync('afplay', [filePath], {
44
+ encoding: 'utf-8',
45
+ })
46
+ if (result.error) throw result.error
47
+ if (result.status !== 0) {
48
+ throw new Error((result.stderr || result.stdout || 'afplay failed').trim())
49
+ }
50
+ }
51
+
52
+ function playLinux(filePath) {
53
+ const script = `if command -v aplay >/dev/null 2>&1; then aplay -q ${shellQuote(filePath)}; elif command -v paplay >/dev/null 2>&1; then paplay ${shellQuote(filePath)}; else printf '\\a'; fi`
54
+ const result = spawnSync('sh', ['-c', script], {
55
+ encoding: 'utf-8',
56
+ })
57
+ if (result.error) throw result.error
58
+ if (result.status !== 0) {
59
+ throw new Error((result.stderr || result.stdout || 'Linux sound playback failed').trim())
60
+ }
61
+ }
62
+
63
+ export function playSoundEvent(event = '', pkgRoot = PKG_ROOT) {
64
+ const soundPath = resolveSoundPath(event, pkgRoot)
65
+ if (!soundPath) {
66
+ process.stderr.write('\x07')
67
+ return false
68
+ }
69
+
70
+ if (PLAT === 'win32') {
71
+ playWindows(soundPath)
72
+ return true
73
+ }
74
+ if (PLAT === 'darwin') {
75
+ playMac(soundPath)
76
+ return true
77
+ }
78
+
79
+ playLinux(soundPath)
80
+ return true
81
+ }
82
+
83
+ function main() {
84
+ const event = process.argv[2] || 'complete'
85
+ try {
86
+ playSoundEvent(event)
87
+ } catch {
88
+ process.stderr.write('\x07')
89
+ }
90
+ }
91
+
92
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
93
+ main()
94
+ }
@@ -5,7 +5,7 @@
5
5
  import { platform } from 'node:os';
6
6
  import { join } from 'node:path';
7
7
  import { existsSync } from 'node:fs';
8
- import { spawnSync } from 'node:child_process';
8
+ import { execFileSync, spawn } from 'node:child_process';
9
9
 
10
10
  const PLAT = platform();
11
11
 
@@ -55,33 +55,65 @@ function resolveWav(pkgRoot, event) {
55
55
  return existsSync(p) ? p : null;
56
56
  }
57
57
 
58
- function runSync(command, args) {
58
+ function resolveSoundHelper(pkgRoot) {
59
+ const helperPath = join(pkgRoot, 'scripts', 'notify-sound.mjs');
60
+ return existsSync(helperPath) ? helperPath : '';
61
+ }
62
+
63
+ function runDetached(command, args) {
59
64
  try {
60
- const result = spawnSync(command, args, {
65
+ const child = spawn(command, args, {
61
66
  stdio: 'ignore',
67
+ detached: true,
62
68
  windowsHide: true,
63
69
  });
64
- return !result.error && result.status === 0;
70
+ child.on('error', () => {});
71
+ child.unref();
72
+ return true;
65
73
  } catch {
66
74
  return false;
67
75
  }
68
76
  }
69
77
 
70
- export function playSound(pkgRoot, event) {
78
+ function shellQuote(value = '') {
79
+ return `'${String(value).replace(/'/g, `'\\''`)}'`
80
+ }
81
+
82
+ function runSoundHelper(pkgRoot, event, mode = 'background') {
83
+ const helperPath = resolveSoundHelper(pkgRoot);
84
+ if (!helperPath) return false;
85
+
86
+ if (mode === 'blocking') {
87
+ try {
88
+ execFileSync(process.execPath, [helperPath, event], {
89
+ stdio: 'ignore',
90
+ windowsHide: true,
91
+ });
92
+ return true;
93
+ } catch {
94
+ return false;
95
+ }
96
+ }
97
+
98
+ return runDetached(process.execPath, [helperPath, event]);
99
+ }
100
+
101
+ export function playSound(pkgRoot, event, options = {}) {
71
102
  if (DISABLE_OS_NOTIFICATIONS) return;
72
103
  const wav = resolveWav(pkgRoot, event);
73
104
  if (!wav) { process.stderr.write('\x07'); return; }
105
+ if (runSoundHelper(pkgRoot, event, options.mode === 'blocking' ? 'blocking' : 'background')) return;
74
106
  try {
75
107
  if (PLAT === 'win32') {
76
- runSync('powershell', [
108
+ runDetached('powershell', [
77
109
  '-NoProfile',
78
110
  '-c',
79
111
  `(New-Object Media.SoundPlayer '${wav.replace(/'/g, "''")}').PlaySync()`,
80
112
  ]);
81
113
  } else if (PLAT === 'darwin') {
82
- runSync('afplay', [wav]);
114
+ runDetached('afplay', [wav]);
83
115
  } else {
84
- if (!runSync('aplay', ['-q', wav]) && !runSync('paplay', [wav])) process.stderr.write('\x07');
116
+ runDetached('sh', ['-c', `if command -v aplay >/dev/null 2>&1; then aplay -q ${shellQuote(wav)}; elif command -v paplay >/dev/null 2>&1; then paplay ${shellQuote(wav)}; else printf '\\a'; fi`]);
85
117
  }
86
118
  } catch { process.stderr.write('\x07'); }
87
119
  }
@@ -124,16 +156,16 @@ export function desktopNotify(pkgRoot, event, extra) {
124
156
  try {
125
157
  if (PLAT === 'win32') {
126
158
  const iconPath = join(pkgRoot, 'assets', 'icons', 'icon.png').replace(/\//g, '\\');
127
- runSync('powershell', ['-NoProfile', '-c', buildWindowsToastScript(notification, iconPath)]);
159
+ runDetached('powershell', ['-NoProfile', '-c', buildWindowsToastScript(notification, iconPath)]);
128
160
  } else if (PLAT === 'darwin') {
129
161
  const subtitle = notification.sourceLabel
130
162
  ? ` subtitle "${escapeAppleScriptText(notification.sourceLabel)}"`
131
163
  : '';
132
- runSync('osascript', ['-e',
164
+ runDetached('osascript', ['-e',
133
165
  `display notification "${escapeAppleScriptText(notification.message)}" with title "${escapeAppleScriptText(notification.title)}"${subtitle}`],
134
166
  );
135
167
  } else {
136
- if (!runSync('notify-send', [notification.title, notification.body])) process.stderr.write('\x07');
168
+ runDetached('sh', ['-c', `if command -v notify-send >/dev/null 2>&1; then notify-send ${shellQuote(notification.title)} ${shellQuote(notification.body)}; else printf '\\a'; fi`]);
137
169
  }
138
170
  } catch { process.stderr.write('\x07'); }
139
171
  }