codex-notify 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +74 -1
- data/lib/codex_notify/hook_runner.rb +7 -0
- data/lib/codex_notify/hook_store.rb +5 -3
- data/lib/codex_notify/internal_task.rb +47 -0
- data/lib/codex_notify/stream_processor.rb +34 -22
- data/lib/codex_notify/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a9f20095aeedc816a39f3c4cafe9fffeabb35b56783d0e4c35ccc6362c3320b7
|
|
4
|
+
data.tar.gz: e8ce3c1e7a9035451d705fef8c8d6a3a2f28bcb4a90df0cfe3c58dce086368d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b74d3ea07ae06575e8e95f3cffba1b7add24f8565f7ff768ee0a75b5ba72b49a621a78262bca324a9452fa20a61ac3f1876d50d194b2d4ebfe44ec418a9cbd99
|
|
7
|
+
data.tar.gz: 8f70b6c4e10bba04b8077cacae2fd14779e563af69f8f1c5a369edae7a324b1453c33fb94011c99e2c961ba8913c89e9f3910a536f13efb8ad8c9ea59400c94b
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
All notable changes to codex-notify are documented in this file. Releases from
|
|
4
4
|
1.0.0 onward follow [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [1.0.1] - 2026-10-28
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- Suppress known internal Codex task-title and Composer Predictions requests
|
|
11
|
+
and their responses in Hook normal mode and log-tail mode, reducing internal
|
|
12
|
+
activity sent to Slack while preserving ordinary prompts and quoted text.
|
|
13
|
+
- Preserve existing Hook conversation threads when suppressing these internal
|
|
14
|
+
requests, and resume notifications on the next ordinary user prompt.
|
|
15
|
+
- Carry initial internal-prompt suppression to the first identified log
|
|
16
|
+
session, including when the request is not repeated in the log.
|
|
17
|
+
|
|
18
|
+
### Development and security
|
|
19
|
+
|
|
20
|
+
- Add dependency audits before CI tests and package verification, daily and
|
|
21
|
+
manually triggered audits, and GitHub Dependency Review on pull requests.
|
|
22
|
+
- Audit locked gems and the recorded Bundler version, require fresh advisory
|
|
23
|
+
data, and prevent project advisory ignores from hiding known vulnerabilities.
|
|
24
|
+
- Update the preferred development Ruby and CI/release jobs to Ruby 4.0.7.
|
|
25
|
+
Ruby 3.4 or newer remains supported; no configuration migration is required.
|
|
26
|
+
|
|
6
27
|
## [1.0.0] - 2026-08-13
|
|
7
28
|
|
|
8
29
|
### Added
|
|
@@ -47,4 +68,5 @@ All notable changes to codex-notify are documented in this file. Releases from
|
|
|
47
68
|
`command -v codex-notify-hook`, then review and trust the changed Hook
|
|
48
69
|
definition in Codex.
|
|
49
70
|
|
|
71
|
+
[1.0.1]: https://github.com/koichiro/codex-notify/releases/tag/v1.0.1
|
|
50
72
|
[1.0.0]: https://github.com/koichiro/codex-notify/releases/tag/v1.0.0
|
data/README.md
CHANGED
|
@@ -675,6 +675,41 @@ Notes:
|
|
|
675
675
|
|
|
676
676
|
Hook mode does not require `--no-alt-screen`, because it does not depend on session-log tailing.
|
|
677
677
|
|
|
678
|
+
### Internal title-generation and composer-prediction notifications
|
|
679
|
+
|
|
680
|
+
Hook normal mode and log-tail mode suppress the known Codex task-title request
|
|
681
|
+
and subsequent events from that internal session (Hook: the final response;
|
|
682
|
+
log-tail: responses and tool events). Detection requires the complete known
|
|
683
|
+
instruction envelope followed by `User prompt:` and a non-empty prompt. Ordinary
|
|
684
|
+
title requests, JSON responses containing `title`, and instructions quoted with
|
|
685
|
+
an introduction, block quote, or code fence remain eligible for notification.
|
|
686
|
+
An exact copy of the internal request is indistinguishable from that request;
|
|
687
|
+
changed or unknown internal templates are not suppressed automatically.
|
|
688
|
+
|
|
689
|
+
Hook normal mode and log-tail mode also suppress the known Codex Composer
|
|
690
|
+
Predictions request, which generates a suggested next user message, and
|
|
691
|
+
subsequent internal responses.
|
|
692
|
+
Detection requires the complete known instruction template, including its JSON
|
|
693
|
+
schema, with whitespace normalized. Ordinary requests for suggestions, responses
|
|
694
|
+
containing `suggestion`, and templates quoted with an introduction, block quote,
|
|
695
|
+
or code fence remain eligible for notification. Changed or unknown prediction
|
|
696
|
+
templates are not suppressed automatically, and an exact copy is
|
|
697
|
+
indistinguishable from the internal request.
|
|
698
|
+
|
|
699
|
+
Hook suppression persists across invocations and resume events. The next regular
|
|
700
|
+
prompt clears it; startup/clear events retain their existing reset behavior.
|
|
701
|
+
Suppressing title generation or composer prediction preserves an existing
|
|
702
|
+
conversation thread. Hook debug mode retains its existing diagnostic visibility, including these internal
|
|
703
|
+
requests and responses. Log-tail suppression lasts for the current monitor run
|
|
704
|
+
and applies even with tool notifications enabled; the monitoring-start message
|
|
705
|
+
is unchanged. An internal initial prompt supplied with `--prompt` also suppresses
|
|
706
|
+
responses after the first log session ID becomes available, until an ordinary
|
|
707
|
+
user event in that session clears suppression. Starting a monitor after the
|
|
708
|
+
internal prompt cannot classify its response from JSON shape alone.
|
|
709
|
+
|
|
710
|
+
This filter limits unnecessary outbound content. Existing secret redaction
|
|
711
|
+
still applies immediately before Slack API calls.
|
|
712
|
+
|
|
678
713
|
## Development
|
|
679
714
|
|
|
680
715
|
Install development dependencies in a source checkout:
|
|
@@ -695,7 +730,9 @@ are checkout development commands.
|
|
|
695
730
|
|
|
696
731
|
The test suite uses `minitest`, runs through `rake`, and enforces 90% line coverage for files under `lib/`.
|
|
697
732
|
|
|
698
|
-
Ruby 3.4 or newer is supported.
|
|
733
|
+
Ruby 3.4 or newer is supported. The preferred development version in
|
|
734
|
+
`.ruby-version` is Ruby 4.0.7. CI covers the minimum supported 3.4 series and
|
|
735
|
+
Ruby 4.0.7; release jobs also use Ruby 4.0.7.
|
|
699
736
|
|
|
700
737
|
Run either executable from a source checkout through Bundler:
|
|
701
738
|
|
|
@@ -709,6 +746,42 @@ These commands run the checkout code. In contrast, unqualified
|
|
|
709
746
|
Ruby environment. Checkout Bundler and `.ruby-version` behavior is not a
|
|
710
747
|
runtime requirement of the installed commands.
|
|
711
748
|
|
|
749
|
+
### Dependency security checks
|
|
750
|
+
|
|
751
|
+
Dependabot checks Bundler and GitHub Actions updates weekly. CI also runs a
|
|
752
|
+
dependency audit on every push and pull request before tests and package
|
|
753
|
+
verification. The audit checks all gems in `Gemfile.lock`, including development
|
|
754
|
+
and transitive dependencies, plus the Bundler version recorded under
|
|
755
|
+
`BUNDLED WITH`, against a freshly updated Ruby Advisory Database:
|
|
756
|
+
|
|
757
|
+
```bash
|
|
758
|
+
bundle exec ruby script/dependency_audit.rb
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
The same audit runs daily at 09:17 Japan time (00:17 UTC) on the default branch
|
|
762
|
+
and can be started manually from Actions → Dependency audit → Run workflow.
|
|
763
|
+
Scheduled runs begin after the workflow is merged into the default branch.
|
|
764
|
+
Known vulnerabilities, insecure gem sources, and advisory database update
|
|
765
|
+
failures fail the audit rather than being ignored. Local audits require network
|
|
766
|
+
access; the test suite itself remains offline. The audit script requires an
|
|
767
|
+
existing advisory database to be a Git repository and refresh successfully.
|
|
768
|
+
Unlike stock `bundle-audit check --update`, which may warn and continue with
|
|
769
|
+
stale data when the database is not a Git repository, this script fails in that
|
|
770
|
+
case. A missing database is downloaded before scanning. A missing `BUNDLED WITH`
|
|
771
|
+
version also fails the audit instead of silently skipping Bundler. Advisory
|
|
772
|
+
`ignore` entries in a project's `.bundler-audit.yml` do not suppress findings
|
|
773
|
+
for locked gems or Bundler in this audit.
|
|
774
|
+
|
|
775
|
+
Pull requests additionally run GitHub's Dependency Review Action against the
|
|
776
|
+
GitHub Advisory Database, which also powers Dependabot alerts. Newly introduced
|
|
777
|
+
known vulnerabilities of low severity or higher fail the review for runtime,
|
|
778
|
+
development, and unknown dependency scopes. Results appear in Actions logs and
|
|
779
|
+
job summaries. These checks supplement Dependabot; they do not change repository
|
|
780
|
+
alert or automatic security-update settings. Dependency Review requires the
|
|
781
|
+
repository's Dependency graph to be enabled under Settings → Advanced Security;
|
|
782
|
+
enabling Dependabot alerts also enables the graph. To require successful checks
|
|
783
|
+
before merging, configure branch protection or repository rulesets separately.
|
|
784
|
+
|
|
712
785
|
### Gem packaging
|
|
713
786
|
|
|
714
787
|
The gem version is defined once as `CodexNotify::VERSION`. The initial package
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'hook_config'
|
|
4
|
+
require_relative 'internal_task'
|
|
4
5
|
require_relative 'hook_store'
|
|
5
6
|
require_relative 'hook_formatter'
|
|
6
7
|
require_relative 'hook_thread_publisher'
|
|
@@ -89,6 +90,12 @@ module CodexNotify
|
|
|
89
90
|
def handle_user_prompt_submit(event)
|
|
90
91
|
prompt = event.prompt
|
|
91
92
|
session_id = event.session_id
|
|
93
|
+
reason = InternalTask.suppression_reason(prompt) if normal?
|
|
94
|
+
if reason
|
|
95
|
+
@store.suppress_session(session_id, reason, preserve_thread: true)
|
|
96
|
+
return
|
|
97
|
+
end
|
|
98
|
+
|
|
92
99
|
if normal? && internal_ambient_suggestions_prompt?(prompt)
|
|
93
100
|
@store.suppress_session(session_id, 'internal_ambient_suggestions')
|
|
94
101
|
return
|
|
@@ -28,11 +28,13 @@ module CodexNotify
|
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def suppress_session(session_id, reason)
|
|
31
|
+
def suppress_session(session_id, reason, preserve_thread: false)
|
|
32
32
|
update_state do |data|
|
|
33
33
|
key = session_id.to_s
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
unless preserve_thread
|
|
35
|
+
data['threads'].delete(key)
|
|
36
|
+
data['generations'][key] = data['generations'].fetch(key, 0).to_i + 1
|
|
37
|
+
end
|
|
36
38
|
data['suppressed_sessions'][key] = {
|
|
37
39
|
'reason' => reason.to_s,
|
|
38
40
|
'suppressed_at' => Time.now.utc.iso8601
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CodexNotify
|
|
4
|
+
module InternalTask
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
# Match the complete known envelope, not title-related words or JSON output.
|
|
8
|
+
TITLE_INSTRUCTIONS = <<~TEXT.split.join(' ').freeze
|
|
9
|
+
You are a helpful assistant. You will be presented with a user prompt, and your job is to provide a short title for a task that will be created from that prompt.
|
|
10
|
+
The task usually has to do with coding work, such as fixing a bug, changing a feature, or answering a question about a codebase.
|
|
11
|
+
Generate a concise UI title of at most 36 characters.
|
|
12
|
+
Use a single line of plain text only.
|
|
13
|
+
Do not include quotes, markdown, formatting characters, or trailing punctuation.
|
|
14
|
+
If the prompt includes a ticket reference, include it verbatim.
|
|
15
|
+
Prefer an imperative verb when the user is asking for a change.
|
|
16
|
+
Do not answer the user or attempt the task.
|
|
17
|
+
TEXT
|
|
18
|
+
|
|
19
|
+
COMPOSER_PREDICTION_INSTRUCTIONS = <<~TEXT.split.join(' ').freeze
|
|
20
|
+
This is an app-generated request for a suggested next user message. The user did not write this message.
|
|
21
|
+
|
|
22
|
+
Your suggestion will appear in the user's empty message composer. They can accept it, edit it, or ignore it; it will not be sent automatically.
|
|
23
|
+
|
|
24
|
+
Predict the single message this user is most likely to type next from the visible conversation. Use the whole conversation to understand their current intent and the latest exchange. Choose the action and specific object that best continue that intent, rather than a generally useful follow-up. Express its core naturally in the user's voice, keeping the requirements needed to retain its meaning. Choose the strongest grounded move even if several continuations are possible; uncertainty about exact wording or incidental details is not a reason to return null. Use null only when the conversation offers no grounded next move at all.
|
|
25
|
+
|
|
26
|
+
Set suggestion to exactly one natural user message, one line, at most 240 characters, or null. No alternatives or explanation.
|
|
27
|
+
|
|
28
|
+
Return only JSON matching this schema: {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"suggestion":{"type":["string","null"]}},"required":["suggestion"],"additionalProperties":false}.
|
|
29
|
+
TEXT
|
|
30
|
+
|
|
31
|
+
def suppression_reason(prompt)
|
|
32
|
+
return 'internal_title_generation' if title_prompt?(prompt)
|
|
33
|
+
return 'internal_composer_prediction' if composer_prediction_prompt?(prompt)
|
|
34
|
+
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def composer_prediction_prompt?(prompt)
|
|
39
|
+
prompt.to_s.split.join(' ') == COMPOSER_PREDICTION_INSTRUCTIONS
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def title_prompt?(prompt)
|
|
43
|
+
instructions, separator, user_prompt = prompt.to_s.strip.partition(/\r?\n\s*User prompt:\s*\r?\n/)
|
|
44
|
+
!separator.empty? && !user_prompt.strip.empty? && instructions.split.join(' ') == TITLE_INSTRUCTIONS
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
+
require_relative 'internal_task'
|
|
4
5
|
|
|
5
6
|
module CodexNotify
|
|
6
7
|
module StreamProcessor
|
|
@@ -8,6 +9,13 @@ module CodexNotify
|
|
|
8
9
|
|
|
9
10
|
def process_codex_log_stream(stream, token:, channel:, root_message:, initial_prompt: nil, user_name: 'user', include_tools: false,
|
|
10
11
|
throttle_sec: 0.0, post_func:, sleep_func: Kernel.method(:sleep), publisher: nil)
|
|
12
|
+
suppressed_sessions = {}
|
|
13
|
+
if InternalTask.suppression_reason(initial_prompt)
|
|
14
|
+
suppressed_sessions['__default__'] = true
|
|
15
|
+
initial_prompt = nil
|
|
16
|
+
end
|
|
17
|
+
stream = visible_log_events(stream, suppressed_sessions)
|
|
18
|
+
|
|
11
19
|
if publisher
|
|
12
20
|
return process_durable_stream(
|
|
13
21
|
stream, root_message:, initial_prompt:, user_name:, include_tools:, publisher:
|
|
@@ -43,15 +51,7 @@ module CodexNotify
|
|
|
43
51
|
thread_ts_by_session['__default__'] = post_root.call(message)
|
|
44
52
|
end
|
|
45
53
|
|
|
46
|
-
stream.each do |
|
|
47
|
-
line = raw.strip
|
|
48
|
-
next if line.empty?
|
|
49
|
-
|
|
50
|
-
event = JSON.parse(line)
|
|
51
|
-
session_id = CodexNotify::LogEventParser.extract_session_id(event) || '__default__'
|
|
52
|
-
extracted_events = CodexNotify::LogEventParser.extract_events(event)
|
|
53
|
-
next if extracted_events.empty?
|
|
54
|
-
|
|
54
|
+
stream.each do |session_id, extracted_events|
|
|
55
55
|
extracted_events.each do |kind, text, part_type|
|
|
56
56
|
next if text.empty?
|
|
57
57
|
next if part_type == 'tool' && !include_tools
|
|
@@ -78,8 +78,6 @@ module CodexNotify
|
|
|
78
78
|
|
|
79
79
|
post_message.call(message, thread_ts)
|
|
80
80
|
end
|
|
81
|
-
rescue JSON::ParserError
|
|
82
|
-
next
|
|
83
81
|
end
|
|
84
82
|
|
|
85
83
|
0
|
|
@@ -97,15 +95,7 @@ module CodexNotify
|
|
|
97
95
|
return 1 unless drain_succeeded?(publisher)
|
|
98
96
|
end
|
|
99
97
|
|
|
100
|
-
stream.each do |
|
|
101
|
-
line = raw.strip
|
|
102
|
-
next if line.empty?
|
|
103
|
-
|
|
104
|
-
event = JSON.parse(line)
|
|
105
|
-
session_id = CodexNotify::LogEventParser.extract_session_id(event) || '__default__'
|
|
106
|
-
extracted_events = CodexNotify::LogEventParser.extract_events(event)
|
|
107
|
-
next if extracted_events.empty?
|
|
108
|
-
|
|
98
|
+
stream.each do |session_id, extracted_events|
|
|
109
99
|
extracted_events.each do |kind, body, part_type|
|
|
110
100
|
next if body.empty?
|
|
111
101
|
next if part_type == 'tool' && !include_tools
|
|
@@ -126,13 +116,35 @@ module CodexNotify
|
|
|
126
116
|
end
|
|
127
117
|
return 1 unless drain_succeeded?(publisher)
|
|
128
118
|
end
|
|
129
|
-
rescue JSON::ParserError
|
|
130
|
-
next
|
|
131
119
|
end
|
|
132
120
|
|
|
133
121
|
0
|
|
134
122
|
end
|
|
135
123
|
|
|
124
|
+
def visible_log_events(stream, suppressed_sessions)
|
|
125
|
+
Enumerator.new do |output|
|
|
126
|
+
current_session = '__default__'
|
|
127
|
+
stream.each do |raw|
|
|
128
|
+
begin
|
|
129
|
+
event = JSON.parse(raw)
|
|
130
|
+
rescue JSON::ParserError
|
|
131
|
+
next
|
|
132
|
+
end
|
|
133
|
+
session_id = LogEventParser.extract_session_id(event)
|
|
134
|
+
if session_id && current_session == '__default__' && suppressed_sessions.key?('__default__')
|
|
135
|
+
# Bind anonymous suppression to the first session, not every later ID.
|
|
136
|
+
suppressed_sessions[session_id] = suppressed_sessions.delete('__default__')
|
|
137
|
+
end
|
|
138
|
+
current_session = session_id || current_session
|
|
139
|
+
events = LogEventParser.extract_events(event).reject do |kind, text, _type|
|
|
140
|
+
suppressed_sessions[current_session] = InternalTask.suppression_reason(text) if kind == 'user'
|
|
141
|
+
suppressed_sessions[current_session]
|
|
142
|
+
end
|
|
143
|
+
output << [session_id || '__default__', events] unless events.empty?
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
136
148
|
def drain_succeeded?(publisher)
|
|
137
149
|
result = publisher.drain
|
|
138
150
|
result.failed.empty? && result.needs_review.empty?
|
data/lib/codex_notify/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: codex-notify
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Koichiro Ohba
|
|
@@ -56,6 +56,7 @@ files:
|
|
|
56
56
|
- lib/codex_notify/hook_runner.rb
|
|
57
57
|
- lib/codex_notify/hook_store.rb
|
|
58
58
|
- lib/codex_notify/hook_thread_publisher.rb
|
|
59
|
+
- lib/codex_notify/internal_task.rb
|
|
59
60
|
- lib/codex_notify/log_event_parser.rb
|
|
60
61
|
- lib/codex_notify/message_formatter.rb
|
|
61
62
|
- lib/codex_notify/outbox_commands.rb
|
|
@@ -71,9 +72,9 @@ homepage: https://github.com/koichiro/codex-notify
|
|
|
71
72
|
licenses:
|
|
72
73
|
- MIT
|
|
73
74
|
metadata:
|
|
74
|
-
source_code_uri: https://github.com/koichiro/codex-notify/tree/v1.0.
|
|
75
|
+
source_code_uri: https://github.com/koichiro/codex-notify/tree/v1.0.1
|
|
75
76
|
bug_tracker_uri: https://github.com/koichiro/codex-notify/issues
|
|
76
|
-
changelog_uri: https://github.com/koichiro/codex-notify/blob/v1.0.
|
|
77
|
+
changelog_uri: https://github.com/koichiro/codex-notify/blob/v1.0.1/CHANGELOG.md
|
|
77
78
|
documentation_uri: https://github.com/koichiro/codex-notify#readme
|
|
78
79
|
rubygems_mfa_required: 'true'
|
|
79
80
|
rdoc_options: []
|
|
@@ -90,7 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
90
91
|
- !ruby/object:Gem::Version
|
|
91
92
|
version: '0'
|
|
92
93
|
requirements: []
|
|
93
|
-
rubygems_version: 4.0.
|
|
94
|
+
rubygems_version: 4.0.20
|
|
94
95
|
specification_version: 4
|
|
95
96
|
summary: Send compact Codex activity notifications to Slack
|
|
96
97
|
test_files: []
|