sneakoscope 0.9.1 → 0.9.2
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/README.md +13 -1
- package/package.json +1 -1
- package/src/cli/main.mjs +10 -3
- package/src/core/codex-app.mjs +31 -3
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +95 -2
package/README.md
CHANGED
|
@@ -288,7 +288,9 @@ For headless remotely controllable Codex App/server sessions on Codex CLI 0.130.
|
|
|
288
288
|
sks codex-app remote-control -- --help
|
|
289
289
|
```
|
|
290
290
|
|
|
291
|
-
`sks codex-app check` reports whether the installed Codex CLI is new enough, whether the required app flags are visible, whether Fast/speed-selector config is unlocked, and whether installed OpenAI default plugins such as Browser, Chrome, Computer Use, Documents, Presentations, Spreadsheets, and LaTeX are enabled. When codex-lb is configured, SKS keeps it selected as the top-level Codex App provider while still preserving required app flags and plugin settings. Codex CLI 0.130.0+ app-server/remote-control threads can pick up config changes live; older CLI/TUI sessions should still be restarted after `.codex/config.toml` or MCP/plugin changes.
|
|
291
|
+
`sks codex-app check` reports whether the installed Codex CLI is new enough, whether the required app flags are visible, whether Fast/speed-selector config is unlocked, whether Codex App Git Actions can use Commit, Push, Commit and Push, and PR flows, and whether installed OpenAI default plugins such as Browser, Chrome, Computer Use, Documents, Presentations, Spreadsheets, and LaTeX are enabled. When codex-lb is configured, SKS keeps it selected as the top-level Codex App provider while still preserving required app flags and plugin settings. Codex CLI 0.130.0+ app-server/remote-control threads can pick up config changes live; older CLI/TUI sessions should still be restarted after `.codex/config.toml` or MCP/plugin changes.
|
|
292
|
+
|
|
293
|
+
Image-review routes are intentionally strict. `$Image-UX-Review`, `$UX-Review`, `$Visual-Review`, and `$UI-UX-Review` require real Codex App `$imagegen`/`gpt-image-2` generated annotated review images before `image-ux-review-gate.json` can pass; disabled or missing `image_generation` remains a blocker that `sks codex-app check` and selftest cover.
|
|
292
294
|
|
|
293
295
|
Then open Codex App and use prompt commands directly in the chat. Examples:
|
|
294
296
|
|
|
@@ -297,6 +299,7 @@ $Team implement the checkout fix and verify it
|
|
|
297
299
|
$DFix change this label and spacing only
|
|
298
300
|
$QA-LOOP dogfood localhost:3000 and fix safe issues
|
|
299
301
|
$PPT create an investor deck as HTML/PDF
|
|
302
|
+
$UX-Review this screenshot with gpt-image-2 callouts, then fix the issues
|
|
300
303
|
$Goal persist this migration workflow with native /goal continuation
|
|
301
304
|
$Research investigate this mechanism with source-backed scout lenses
|
|
302
305
|
$Wiki refresh and validate the context pack
|
|
@@ -420,6 +423,15 @@ codex mcp list
|
|
|
420
423
|
|
|
421
424
|
Codex App workflows need the app installed. UI/browser evidence requires first-party Codex Computer Use, and generated raster/image-review evidence requires real `$imagegen`/`gpt-image-2` output. After setup/upgrade, start a fresh thread so Codex reloads plugin tools.
|
|
422
425
|
|
|
426
|
+
### Codex App commit/push is blocked
|
|
427
|
+
|
|
428
|
+
```sh
|
|
429
|
+
sks doctor --fix
|
|
430
|
+
sks codex-app check
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
`sks codex-app check` now prints `Git Actions`. It should be `ok` for Codex App Commit, Push, Commit and Push, and PR buttons to bypass SKS route gates. If it is blocked, repair config with `sks doctor --fix`; if the blocker mentions remote-control, update Codex CLI to `0.130.0` or newer and restart older app-server/TUI sessions.
|
|
434
|
+
|
|
423
435
|
### Codex App UI looks stale after codex-lb changes
|
|
424
436
|
|
|
425
437
|
If Codex App UI panels or auth-dependent controls still look wrong after codex-lb setup, repair, or upgrade, restart the app first. If the UI still does not recover, sign out of Codex App, sign back in, then run `sks codex-app check` or `sks codex-lb repair` as needed.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.2",
|
|
5
5
|
"description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Goal, AutoResearch, TriWiki, and Honest Mode.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
|
package/src/cli/main.mjs
CHANGED
|
@@ -1648,7 +1648,7 @@ async function setup(args) {
|
|
|
1648
1648
|
else console.log('Git: .gitignore ignores SKS generated files');
|
|
1649
1649
|
console.log(`Codex App: .codex/config.toml, .codex/hooks.json, .agents/skills, .codex/agents, .codex/SNEAKOSCOPE.md`);
|
|
1650
1650
|
console.log(`Global $: ${globalSkills.status === 'installed' ? 'ok' : globalSkills.status} ${globalSkills.root || ''}`.trimEnd());
|
|
1651
|
-
console.log(`App tools: ${appRuntime.ok ? 'ok' : 'needs setup'} Codex App=${appRuntime.app.installed ? 'ok' : 'missing'} Browser=${appRuntime.features?.browser_tool_ready ? 'ok' : 'missing'} Computer Use=${appRuntime.mcp.has_computer_use ? 'ok' : 'missing'} Image Gen=${appRuntime.features?.image_generation ? 'ok' : 'missing'}`);
|
|
1651
|
+
console.log(`App tools: ${appRuntime.ok ? 'ok' : 'needs setup'} Codex App=${appRuntime.app.installed ? 'ok' : 'missing'} Browser=${appRuntime.features?.browser_tool_ready ? 'ok' : 'missing'} Computer Use=${appRuntime.mcp.has_computer_use ? 'ok' : 'missing'} Image Gen=${appRuntime.features?.image_generation ? 'ok' : 'missing'} Git Actions=${appRuntime.features?.git_actions?.ok ? 'ok' : 'missing'}`);
|
|
1652
1652
|
console.log(`Prompt: intent-first routing, $Answer fact-check route, $DFix ultralight Direct Fix route, $PPT HTML/PDF presentation route, Context7 gate`);
|
|
1653
1653
|
console.log(`Skills: .agents/skills`);
|
|
1654
1654
|
console.log(`Next: sks context7 check; sks selftest --mock; sks commands; sks dollar-commands`);
|
|
@@ -3086,7 +3086,9 @@ async function selftest() {
|
|
|
3086
3086
|
await fsp.chmod(fakeCodex, 0o755);
|
|
3087
3087
|
const codexAppFixtureOpts = { codex: { bin: fakeCodex, version: 'codex-cli 99.0.0' }, home: appFeatureTmp, cwd: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } };
|
|
3088
3088
|
const codexAppFeatureStatus = await codexAppIntegrationStatus(codexAppFixtureOpts);
|
|
3089
|
-
if (!codexAppFeatureStatus.ok || !codexAppFeatureStatus.features?.required_flags_ok || !codexAppFeatureStatus.features?.codex_git_commit || !codexAppFeatureStatus.features?.remote_control || !codexAppFeatureStatus.features?.fast_mode_config?.ok) throw new Error('selftest: codex-app check did not accept required app feature flags, remote_control, and unlocked Fast UI config');
|
|
3089
|
+
if (!codexAppFeatureStatus.ok || !codexAppFeatureStatus.features?.required_flags_ok || !codexAppFeatureStatus.features?.codex_git_commit || !codexAppFeatureStatus.features?.remote_control || !codexAppFeatureStatus.features?.git_actions?.ok || !codexAppFeatureStatus.features?.fast_mode_config?.ok) throw new Error('selftest: codex-app check did not accept required app feature flags, git actions, remote_control, and unlocked Fast UI config');
|
|
3090
|
+
const codexAppOldCliStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodex, version: 'codex-cli 0.129.0' }, home: appFeatureTmp, cwd: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
|
|
3091
|
+
if (codexAppOldCliStatus.ok || codexAppOldCliStatus.features?.git_actions?.ok || !codexAppOldCliStatus.guidance.some((line) => line.includes('git commit/push actions are blocked'))) throw new Error('selftest: codex-app check did not block commit/push actions on old Codex CLI remote-control');
|
|
3090
3092
|
const missingDefaultPluginTmp = tmpdir();
|
|
3091
3093
|
await ensureDir(path.join(missingDefaultPluginTmp, '.codex'));
|
|
3092
3094
|
const codexConfigWithoutMarketplaceSources = codexConfigText.replace(/(?:^|\n)\[marketplaces\.[^\]\r\n]+\][\s\S]*?(?=\n\[[^\]]+\]|\s*$)/g, '').trim();
|
|
@@ -3107,7 +3109,12 @@ async function selftest() {
|
|
|
3107
3109
|
await writeTextAtomic(fakeCodexMissing, '#!/bin/sh\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then printf "%s\\n" "computer-use enabled" "browser-use enabled"; exit 0; fi\nif [ "$1" = "features" ] && [ "$2" = "list" ]; then cat <<EOF\napps stable true\nbrowser_use stable true\nbrowser_use_external stable true\ncodex_git_commit under development false\ncomputer_use stable true\nfast_mode stable true\nguardian_approval stable true\nhooks stable true\nimage_generation stable true\nin_app_browser stable true\nplugins stable true\nremote_control under development true\ntool_suggest stable true\nEOF\nexit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
3108
3110
|
await fsp.chmod(fakeCodexMissing, 0o755);
|
|
3109
3111
|
const codexAppMissingFeatureStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodexMissing, version: 'codex-cli 99.0.0' }, home: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
|
|
3110
|
-
if (codexAppMissingFeatureStatus.ok || codexAppMissingFeatureStatus.features?.required_flags_ok || codexAppMissingFeatureStatus.features?.codex_git_commit) throw new Error('selftest: codex-app check did not block disabled codex_git_commit feature flag');
|
|
3112
|
+
if (codexAppMissingFeatureStatus.ok || codexAppMissingFeatureStatus.features?.required_flags_ok || codexAppMissingFeatureStatus.features?.codex_git_commit || codexAppMissingFeatureStatus.features?.git_actions?.ok) throw new Error('selftest: codex-app check did not block disabled codex_git_commit feature flag');
|
|
3113
|
+
const fakeCodexMissingImageGen = path.join(fakeCodexBinDir, 'codex-missing-imagegen');
|
|
3114
|
+
await writeTextAtomic(fakeCodexMissingImageGen, '#!/bin/sh\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then printf "%s\\n" "computer-use enabled" "browser-use enabled"; exit 0; fi\nif [ "$1" = "features" ] && [ "$2" = "list" ]; then cat <<EOF\napps stable true\nbrowser_use stable true\nbrowser_use_external stable true\ncodex_git_commit under development true\ncomputer_use stable true\nfast_mode stable true\nguardian_approval stable true\nhooks stable true\nimage_generation stable false\nin_app_browser stable true\nplugins stable true\nremote_control under development true\ntool_suggest stable true\nEOF\nexit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
3115
|
+
await fsp.chmod(fakeCodexMissingImageGen, 0o755);
|
|
3116
|
+
const codexAppMissingImageGenStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodexMissingImageGen, version: 'codex-cli 99.0.0' }, home: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
|
|
3117
|
+
if (codexAppMissingImageGenStatus.ok || codexAppMissingImageGenStatus.features?.required_flags_ok || codexAppMissingImageGenStatus.features?.image_generation || !codexAppMissingImageGenStatus.guidance.some((line) => line.includes('image_generation'))) throw new Error('selftest: codex-app check did not block disabled image_generation for imagegen pipelines');
|
|
3111
3118
|
const autoReviewHome = path.join(tmp, 'auto-review-home');
|
|
3112
3119
|
const autoReviewEnv = { HOME: autoReviewHome };
|
|
3113
3120
|
const autoReviewEnabled = await enableAutoReview({ env: autoReviewEnv, high: true });
|
package/src/core/codex-app.mjs
CHANGED
|
@@ -135,7 +135,8 @@ export async function codexAppIntegrationStatus(opts = {}) {
|
|
|
135
135
|
const browserToolReady = inAppBrowserReady || browserUseFeatureReady || browserUseReady;
|
|
136
136
|
const appInstalled = Boolean(appPath);
|
|
137
137
|
const pluginPickerReady = requiredFeatureFlags.tool_suggest && requiredFeatureFlags.plugins && requiredFeatureFlags.apps && defaultPlugins.ok && pluginSkillShadows.ok && fastModeConfig.ok;
|
|
138
|
-
const
|
|
138
|
+
const gitActions = codexGitActionReadiness({ requiredFeatureFlags, remoteControl });
|
|
139
|
+
const ready = appInstalled && Boolean(codex.bin) && mcpList.ok && featureList.ok && requiredFeatureFlagsOk && pluginPickerReady && fastModeConfig.ok && imageGenerationReady && gitActions.ok && computerUseReady && browserToolReady;
|
|
139
140
|
return {
|
|
140
141
|
ok: ready,
|
|
141
142
|
app: {
|
|
@@ -166,6 +167,7 @@ export async function codexAppIntegrationStatus(opts = {}) {
|
|
|
166
167
|
required_flags: requiredFeatureFlags,
|
|
167
168
|
required_flags_ok: requiredFeatureFlagsOk,
|
|
168
169
|
fast_mode_config: fastModeConfig,
|
|
170
|
+
git_actions: gitActions,
|
|
169
171
|
image_generation: imageGenerationReady,
|
|
170
172
|
image_generation_source: imageGenerationReady ? 'codex_features_list' : 'missing',
|
|
171
173
|
in_app_browser: inAppBrowserReady,
|
|
@@ -196,7 +198,7 @@ export async function codexAppIntegrationStatus(opts = {}) {
|
|
|
196
198
|
fast_mode_config_ok: fastModeConfig.ok
|
|
197
199
|
}
|
|
198
200
|
},
|
|
199
|
-
guidance: codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags, requiredFeatureFlagsOk, defaultPlugins, pluginSkillShadows, fastModeConfig, imageGenerationReady, inAppBrowserReady, browserUseFeatureReady, computerUseReady, browserUseReady, browserToolReady, computerUseMcpListed, browserUseMcpListed, remoteControl })
|
|
201
|
+
guidance: codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags, requiredFeatureFlagsOk, defaultPlugins, pluginSkillShadows, fastModeConfig, gitActions, imageGenerationReady, inAppBrowserReady, browserUseFeatureReady, computerUseReady, browserUseReady, browserToolReady, computerUseMcpListed, browserUseMcpListed, remoteControl })
|
|
200
202
|
};
|
|
201
203
|
}
|
|
202
204
|
|
|
@@ -251,7 +253,7 @@ export function formatCodexRemoteControlStatus(status) {
|
|
|
251
253
|
return lines.filter(Boolean).join('\n');
|
|
252
254
|
}
|
|
253
255
|
|
|
254
|
-
export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags = {}, requiredFeatureFlagsOk = true, defaultPlugins = { ok: true, missing_enabled: [] }, pluginSkillShadows = { ok: true, blocking: [] }, fastModeConfig = { ok: true, blockers: [] }, imageGenerationReady, inAppBrowserReady, browserUseFeatureReady, computerUseReady, browserUseReady, browserToolReady, computerUseMcpListed, browserUseMcpListed, remoteControl }) {
|
|
256
|
+
export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags = {}, requiredFeatureFlagsOk = true, defaultPlugins = { ok: true, missing_enabled: [] }, pluginSkillShadows = { ok: true, blocking: [] }, fastModeConfig = { ok: true, blockers: [] }, gitActions = { ok: true, blockers: [] }, imageGenerationReady, inAppBrowserReady, browserUseFeatureReady, computerUseReady, browserUseReady, browserToolReady, computerUseMcpListed, browserUseMcpListed, remoteControl }) {
|
|
255
257
|
const lines = [];
|
|
256
258
|
if (!appInstalled) {
|
|
257
259
|
lines.push('Install and open Codex App for first-party MCP/plugin tools. SKS tmux launch can still run with Codex CLI alone, but Codex Computer Use and imagegen/gpt-image-2 evidence will be unavailable until Codex App is ready.');
|
|
@@ -298,6 +300,12 @@ export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, re
|
|
|
298
300
|
lines.push(`Codex App speed selector can be hidden or locked by config: ${fastModeConfig.blockers.join(', ')}.`);
|
|
299
301
|
lines.push('Run: sks doctor --fix');
|
|
300
302
|
}
|
|
303
|
+
if (!gitActions?.ok) {
|
|
304
|
+
lines.push(`Codex App git commit/push actions are blocked: ${gitActions?.blockers?.join(', ') || 'git action readiness'}. The app Commit, Push, Commit and Push, and PR flows need codex_git_commit, hooks, remote_control, and Codex CLI remote-control support.`);
|
|
305
|
+
lines.push(`Run: sks doctor --fix; if remote-control is still blocked, update Codex CLI to ${CODEX_REMOTE_CONTROL_MIN_VERSION}+ and restart older app-server/TUI sessions.`);
|
|
306
|
+
} else {
|
|
307
|
+
lines.push('Codex App git actions are enabled for Commit, Push, Commit and Push, and PR flows; SKS hooks treat those app metadata actions as lightweight git UI actions.');
|
|
308
|
+
}
|
|
301
309
|
if (appInstalled && (!computerUseReady || !browserToolReady)) {
|
|
302
310
|
lines.push('Open Codex App settings and enable recommended MCP/plugin tools. Codex CLI 0.130.0+ remote-control/app-server sessions can pick up config changes live; restart older CLI/TUI sessions.');
|
|
303
311
|
lines.push('Required for SKS QA-LOOP UI/browser evidence: Codex Computer Use only. Browser tools can support browsing context, but they do not satisfy UI-level E2E verification.');
|
|
@@ -333,6 +341,7 @@ export function formatCodexAppStatus(status, { includeRaw = false } = {}) {
|
|
|
333
341
|
`Fast UI: ${status.features?.fast_mode_config?.ok ? 'ok' : `locked ${(status.features?.fast_mode_config?.blockers || []).join(', ') || 'config'}`}`,
|
|
334
342
|
`Default Plugins:${status.plugins?.default_plugins?.ok ? ' ok' : ` missing ${defaultPluginMissingSummary(status.plugins?.default_plugins) || 'plugin install/config'}`}`,
|
|
335
343
|
`Plugin Picker:${status.plugins?.picker?.ok ? ' ok' : ` blocked ${pluginPickerBlockers(status).join(', ') || 'config'}`}`,
|
|
344
|
+
`Git Actions:${status.features?.git_actions?.ok ? ' ok' : ` blocked ${(status.features?.git_actions?.blockers || []).join(', ') || 'config'}`}`,
|
|
336
345
|
`Computer Use:${status.mcp.has_computer_use ? status.mcp.computer_use_source === 'plugin_cache' ? ' installed (verify @Computer in thread)' : ' ok' : ' missing'}`,
|
|
337
346
|
`Browser: ${status.features?.browser_tool_ready ? `ok (${status.features.browser_tool_source})` : status.mcp.has_browser_use ? status.mcp.browser_use_source === 'plugin_cache' ? 'installed (plugin scoped)' : 'ok' : 'missing'}`,
|
|
338
347
|
`Image Gen: ${status.features?.image_generation ? 'ok ($imagegen/gpt-image-2)' : status.features?.checked ? 'missing' : 'not checked'}`,
|
|
@@ -369,6 +378,25 @@ function missingRequiredFeatureFlags(flags = {}) {
|
|
|
369
378
|
return REQUIRED_CODEX_APP_FEATURE_FLAGS.filter((name) => flags?.[name] !== true);
|
|
370
379
|
}
|
|
371
380
|
|
|
381
|
+
function codexGitActionReadiness({ requiredFeatureFlags = {}, remoteControl = {} } = {}) {
|
|
382
|
+
const blockers = [];
|
|
383
|
+
if (requiredFeatureFlags.codex_git_commit !== true) blockers.push('codex_git_commit');
|
|
384
|
+
if (requiredFeatureFlags.hooks !== true) blockers.push('hooks');
|
|
385
|
+
if (requiredFeatureFlags.remote_control !== true) blockers.push('remote_control_feature');
|
|
386
|
+
if (!remoteControl?.ok) blockers.push(remoteControl?.reason || 'codex_cli_remote_control');
|
|
387
|
+
const ok = blockers.length === 0;
|
|
388
|
+
return {
|
|
389
|
+
ok,
|
|
390
|
+
blockers,
|
|
391
|
+
commit: ok,
|
|
392
|
+
push: ok,
|
|
393
|
+
commit_push: ok,
|
|
394
|
+
pull_request: ok,
|
|
395
|
+
required_flags: ['codex_git_commit', 'hooks', 'remote_control'],
|
|
396
|
+
remote_control_min_version: CODEX_REMOTE_CONTROL_MIN_VERSION
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
372
400
|
async function codexDefaultPluginStatus(opts = {}) {
|
|
373
401
|
const home = opts.home || os.homedir();
|
|
374
402
|
const cwd = opts.cwd || process.cwd();
|
package/src/core/fsx.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
|
|
8
|
-
export const PACKAGE_VERSION = '0.9.
|
|
8
|
+
export const PACKAGE_VERSION = '0.9.2';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
|
|
@@ -59,6 +59,83 @@ function extractCommand(payload) {
|
|
|
59
59
|
return payload.command || payload.tool_input?.command || payload.toolInput?.command || payload.input?.command || payload.tool?.input?.command || '';
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function codexGitActionMetadataText(payload = {}) {
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const out = [];
|
|
65
|
+
const interesting = new Set([
|
|
66
|
+
'action',
|
|
67
|
+
'intent',
|
|
68
|
+
'operation',
|
|
69
|
+
'permission',
|
|
70
|
+
'description',
|
|
71
|
+
'kind',
|
|
72
|
+
'type',
|
|
73
|
+
'feature',
|
|
74
|
+
'tool_name',
|
|
75
|
+
'toolName',
|
|
76
|
+
'name',
|
|
77
|
+
'label',
|
|
78
|
+
'title',
|
|
79
|
+
'source',
|
|
80
|
+
'event',
|
|
81
|
+
'hook',
|
|
82
|
+
'hook_name',
|
|
83
|
+
'hookName',
|
|
84
|
+
'hook_event_name',
|
|
85
|
+
'hookEventName',
|
|
86
|
+
'id',
|
|
87
|
+
'command'
|
|
88
|
+
]);
|
|
89
|
+
const noisy = new Set([
|
|
90
|
+
'prompt',
|
|
91
|
+
'user_prompt',
|
|
92
|
+
'userPrompt',
|
|
93
|
+
'message',
|
|
94
|
+
'assistant_message',
|
|
95
|
+
'last_assistant_message',
|
|
96
|
+
'response',
|
|
97
|
+
'raw',
|
|
98
|
+
'stdout',
|
|
99
|
+
'stderr'
|
|
100
|
+
]);
|
|
101
|
+
function walk(value, depth = 0, parentKey = '') {
|
|
102
|
+
if (!value || typeof value !== 'object' || depth > 5 || seen.has(value)) return;
|
|
103
|
+
seen.add(value);
|
|
104
|
+
for (const [key, candidate] of Object.entries(value)) {
|
|
105
|
+
if (noisy.has(key)) continue;
|
|
106
|
+
if (typeof candidate === 'string') {
|
|
107
|
+
if (interesting.has(key) || /\b(?:codex[_\s-]*app|git[_\s-]*actions?|codex_git_|gitCommit|gitPush|pull\s+request)\b/i.test(candidate)) {
|
|
108
|
+
out.push(`${key}:${candidate}`);
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (candidate && typeof candidate === 'object') {
|
|
113
|
+
const allowedContainer = interesting.has(key)
|
|
114
|
+
|| /^(?:input|metadata|context|client|thread|session|request|payload|tool|tool_input|toolInput|permission_request|permissionRequest)$/i.test(key)
|
|
115
|
+
|| parentKey;
|
|
116
|
+
if (allowedContainer) walk(candidate, depth + 1, key);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
walk(payload);
|
|
121
|
+
return out.join(' ');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function codexGitActionMetadataSignal(text = '') {
|
|
125
|
+
const s = String(text || '');
|
|
126
|
+
if (!s) return false;
|
|
127
|
+
const action = String(s)
|
|
128
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
129
|
+
.replace(/[_-]+/g, ' ');
|
|
130
|
+
if (/\bcodex\s*app\b[\s\S]{0,120}\bgit\b[\s\S]{0,120}\b(?:action|actions|commit|push|pr|pull request)\b/i.test(action)) return true;
|
|
131
|
+
if (/\bgit\s*actions?\b[\s\S]{0,120}\b(?:commit|push|pr|pull request|commit\s*(?:and|&)\s*push)\b/i.test(action)) return true;
|
|
132
|
+
if (/\bcodex\s*git\s*(?:commit|push|pr|pull request|commit\s*(?:and|&)\s*push)\b/i.test(action)) return true;
|
|
133
|
+
if (/\b(?:git\s*)?(?:commit|push|commit\s*(?:and|&)\s*push|create\s+(?:a\s+)?pull\s+request|pull\s+request|pr)\b/i.test(action)) {
|
|
134
|
+
return /\b(?:action|intent|operation|permission|feature|tool\s*name|source|event|hook|name|label|title|type|kind|id)\s*:/i.test(action);
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
62
139
|
function toolFailed(payload = {}) {
|
|
63
140
|
const candidates = [
|
|
64
141
|
payload.exit_code,
|
|
@@ -335,6 +412,7 @@ function looksLikeUserGitAction(payload = {}) {
|
|
|
335
412
|
const command = extractCommand(payload);
|
|
336
413
|
const haystack = [
|
|
337
414
|
command,
|
|
415
|
+
codexGitActionMetadataText(payload),
|
|
338
416
|
payload.action,
|
|
339
417
|
payload.intent,
|
|
340
418
|
payload.operation,
|
|
@@ -345,6 +423,7 @@ function looksLikeUserGitAction(payload = {}) {
|
|
|
345
423
|
payload.toolName
|
|
346
424
|
].filter(Boolean).join(' ');
|
|
347
425
|
if (/\b(?:reset\s+--hard|clean\s+-[^\s]*f|checkout\s+--|restore\s+|rm\s+|push\s+--force|push\s+-[^\s]*f)\b/i.test(command)) return false;
|
|
426
|
+
if (codexGitActionMetadataSignal(haystack)) return true;
|
|
348
427
|
if (/\bcodex\b[\s_-]*(?:app\s*)?(?:git\s*)?(?:action|commit|push|pr)\b/i.test(haystack)) return true;
|
|
349
428
|
if (!/^\s*git\s+/i.test(command)) return false;
|
|
350
429
|
return /\bgit\s+(?:status|diff|add|commit|push|branch|remote|rev-parse|log)\b/i.test(command);
|
|
@@ -474,7 +553,9 @@ function explicitConversationId(payload = {}) {
|
|
|
474
553
|
|
|
475
554
|
function looksLikeCodexGitAction(payload = {}) {
|
|
476
555
|
const prompt = stripVisibleDecisionAnswerBlocks(extractUserPrompt(payload));
|
|
556
|
+
const metadataText = codexGitActionMetadataText(payload);
|
|
477
557
|
const haystack = [
|
|
558
|
+
metadataText,
|
|
478
559
|
payload.action,
|
|
479
560
|
payload.intent,
|
|
480
561
|
payload.operation,
|
|
@@ -502,9 +583,10 @@ function looksLikeCodexGitAction(payload = {}) {
|
|
|
502
583
|
].filter(Boolean).join(' ');
|
|
503
584
|
const codexAppGitSignal = /\bcodex[_\s-]*app\b[\s\S]{0,80}\bgit\b[\s\S]{0,80}\b(?:action|actions|commit|push|pr)\b/i.test(haystack);
|
|
504
585
|
const gitActionSignal = /\bgit[_\s-]*actions?\b[\s\S]{0,80}\b(?:commit|push|commit[\s_-]*(?:and|&)?[\s_-]*push)\b/i.test(haystack);
|
|
505
|
-
const appSignal =
|
|
586
|
+
const appSignal = codexGitActionMetadataSignal(metadataText)
|
|
587
|
+
|| codexAppGitSignal
|
|
506
588
|
|| gitActionSignal
|
|
507
|
-
|| /\b(?:codex[_\s-]*(?:app[_\s-]*)?)?(?:git[_\s-]*)?(?:commit[_\s-]*message|git[_\s-]*commit|codex_git_commit)\b/i.test(haystack)
|
|
589
|
+
|| /\b(?:codex[_\s-]*(?:app[_\s-]*)?)?(?:git[_\s-]*)?(?:commit[_\s-]*message|git[_\s-]*commit|git[_\s-]*push|git[_\s-]*pr|codex_git_commit|codex_git_push|codex_git_pr)\b/i.test(haystack)
|
|
508
590
|
|| /커밋\s*메시지\s*생성/i.test(haystack);
|
|
509
591
|
const promptSignal = /\bgenerate(?:\s+a)?(?:\s+git)?\s+commit\s+message\b/i.test(prompt)
|
|
510
592
|
|| /\bcommit\s+message\b[\s\S]{0,80}\b(?:staged|diff|changes?|git)\b/i.test(prompt)
|
|
@@ -524,7 +606,9 @@ function looksLikeStockCodexGitActionPrompt(prompt = '') {
|
|
|
524
606
|
|
|
525
607
|
function looksLikeCodexGitActionStopCompletion(last = '', payload = {}) {
|
|
526
608
|
const text = String(last || '').trim();
|
|
609
|
+
const metadataText = codexGitActionMetadataText(payload);
|
|
527
610
|
const haystack = [
|
|
611
|
+
metadataText,
|
|
528
612
|
payload.action,
|
|
529
613
|
payload.intent,
|
|
530
614
|
payload.operation,
|
|
@@ -539,6 +623,7 @@ function looksLikeCodexGitActionStopCompletion(last = '', payload = {}) {
|
|
|
539
623
|
payload.metadata?.feature,
|
|
540
624
|
payload.metadata?.source
|
|
541
625
|
].filter(Boolean).join(' ');
|
|
626
|
+
if (codexGitActionMetadataSignal(metadataText)) return true;
|
|
542
627
|
if (/\bcodex[_\s-]*app\b[\s\S]{0,80}\bgit\b[\s\S]{0,80}\b(?:action|commit|push|pr)\b/i.test(haystack)) return true;
|
|
543
628
|
if (!text || text.length > 180) return false;
|
|
544
629
|
return /^(?:commit(?:ted)?(?:\s+and\s+pushed)?(?:\s+changes)?(?:\s+complete[.!]?)?|push(?:ed)?(?:\s+changes)?(?:\s+complete[.!]?)?|created\s+(?:a\s+)?pull\s+request[.!]?)$/i.test(text);
|
|
@@ -1010,6 +1095,14 @@ export async function selftestCodexCommitHooks() {
|
|
|
1010
1095
|
const appCommitPushStop = await runHook('stop', { conversation_id: commitPushId, last_assistant_message: 'Commit and push complete.' });
|
|
1011
1096
|
if (appCommitPushStop.code !== 0) throw new Error(`selftest failed: app commit-push stop ${appCommitPushStop.code}: ${appCommitPushStop.stderr}`);
|
|
1012
1097
|
if (JSON.parse(appCommitPushStop.stdout).decision === 'block') throw new Error('selftest failed: app commit-push stop bypass');
|
|
1098
|
+
const appPushId = 'app-push-selftest';
|
|
1099
|
+
const appPushHook = await runHook('user-prompt-submit', { conversation_id: appPushId, metadata: { source: 'codex_app', action: 'Git Actions Push' }, prompt: 'Push changes.' });
|
|
1100
|
+
if (appPushHook.code !== 0) throw new Error(`selftest failed: app push hook ${appPushHook.code}: ${appPushHook.stderr}`);
|
|
1101
|
+
const appPushJson = JSON.parse(appPushHook.stdout);
|
|
1102
|
+
if (appPushJson.decision === 'block' || appPushJson.hookSpecificOutput?.additionalContext || !String(appPushJson.systemMessage || '').includes('git action')) throw new Error('selftest failed: app push metadata route bypass');
|
|
1103
|
+
const appPushStop = await runHook('stop', { conversation_id: appPushId, metadata: { source: 'codex_app', action: 'Git Actions Push' }, last_assistant_message: 'Done.' });
|
|
1104
|
+
if (appPushStop.code !== 0) throw new Error(`selftest failed: app push stop ${appPushStop.code}: ${appPushStop.stderr}`);
|
|
1105
|
+
if (JSON.parse(appPushStop.stdout).decision === 'block') throw new Error('selftest failed: app push metadata stop bypass');
|
|
1013
1106
|
const metadataLightId = 'metadata-light-commit-push-selftest';
|
|
1014
1107
|
const metadataLightHook = await runHook('user-prompt-submit', { conversation_id: metadataLightId, prompt: 'Commit and push changes.' });
|
|
1015
1108
|
if (metadataLightHook.code !== 0) throw new Error(`selftest failed: metadata-light commit-push hook ${metadataLightHook.code}: ${metadataLightHook.stderr}`);
|