@agentwhy/cli 0.1.0 → 0.2.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.
- package/LICENSE +1 -1
- package/README.md +57 -5
- package/dist/adapter/claude-code/contract/assumptions.js +16 -4
- package/dist/adapter/claude-code/contract/denials.js +16 -1
- package/dist/adapter/claude-code/contract/desktop-sessions.js +28 -0
- package/dist/adapter/claude-code/contract/entry-points.js +6 -1
- package/dist/adapter/claude-code/contract/fields.js +2 -0
- package/dist/adapter/claude-code/contract/hooks.js +8 -0
- package/dist/adapter/claude-code/contract/identifiers.js +2 -0
- package/dist/adapter/claude-code/contract/layout.js +2 -0
- package/dist/adapter/claude-code/contract/line-types.js +2 -0
- package/dist/adapter/claude-code/contract/projects.js +2 -0
- package/dist/adapter/claude-code/contract/recognition.js +2 -0
- package/dist/adapter/claude-code/contract/settings.js +2 -0
- package/dist/adapter/claude-code/contract/task-notifications.js +2 -0
- package/dist/adapter/claude-code/contract/version.js +14 -1
- package/dist/adapter/claude-code/discovery/claude-code-project-catalogue.js +12 -12
- package/dist/adapter/claude-code/discovery/claude-code-recognition.js +2 -0
- package/dist/adapter/claude-code/discovery/claude-code-session-catalogue.js +28 -7
- package/dist/adapter/claude-code/discovery/claude-code-session-discovery.js +2 -0
- package/dist/adapter/claude-code/discovery/claude-code-session-titles.js +30 -6
- package/dist/adapter/claude-code/discovery/claude-desktop-titles.js +102 -0
- package/dist/adapter/claude-code/discovery/session-entries.js +2 -0
- package/dist/adapter/claude-code/events/delegation-index.js +2 -0
- package/dist/adapter/claude-code/events/transcript-scan.js +6 -5
- package/dist/adapter/claude-code/hooks/hook-output.js +2 -0
- package/dist/adapter/claude-code/hooks/pre-tool-use-input.js +2 -0
- package/dist/adapter/claude-code/hooks/stop-input.js +2 -0
- package/dist/adapter/claude-code/hooks/subagent-stop-input.js +4 -0
- package/dist/adapter/claude-code/probe/claude-code-probe.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/agent-tool-input-key-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/denial-kind-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/denied-call-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/identifier-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/line-type-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/sidechain-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/task-notification-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/tool-result-reference-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/tool-use-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/tool-version-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/top-level-key-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/unknown-line-type-key-collector.js +2 -0
- package/dist/adapter/claude-code/probe/collectors/working-directory-collector.js +2 -0
- package/dist/adapter/claude-code/probe/meta-tally.js +2 -0
- package/dist/adapter/claude-code/probe/transcript-line.js +2 -0
- package/dist/adapter/claude-code/probe/transcript-tally.js +2 -0
- package/dist/adapter/claude-code/settings/deny-entries.js +2 -0
- package/dist/adapter/claude-code/settings/hook-entries.js +9 -0
- package/dist/adapter/claude-code/settings/refuse-project.js +99 -0
- package/dist/adapter/codex/contract/actions.js +2 -0
- package/dist/adapter/codex/contract/assumptions.js +2 -0
- package/dist/adapter/codex/contract/chat-folders.js +19 -0
- package/dist/adapter/codex/contract/delegations.js +2 -0
- package/dist/adapter/codex/contract/deliveries.js +2 -0
- package/dist/adapter/codex/contract/envelope.js +2 -0
- package/dist/adapter/codex/contract/hook-refusals.js +2 -0
- package/dist/adapter/codex/contract/hooks.js +51 -1
- package/dist/adapter/codex/contract/messages.js +2 -0
- package/dist/adapter/codex/contract/permissions.js +2 -0
- package/dist/adapter/codex/contract/reviews.js +2 -0
- package/dist/adapter/codex/contract/session.js +29 -1
- package/dist/adapter/codex/contract/turns.js +2 -0
- package/dist/adapter/codex/contract/version.js +2 -0
- package/dist/adapter/codex/discovery/codex-on-this-computer.js +13 -0
- package/dist/adapter/codex/discovery/codex-project-catalogue.js +2 -12
- package/dist/adapter/codex/discovery/codex-session-catalogue.js +2 -1
- package/dist/adapter/codex/discovery/codex-session-discovery.js +2 -0
- package/dist/adapter/codex/discovery/session-owners.js +2 -0
- package/dist/adapter/codex/events/action-items.js +2 -0
- package/dist/adapter/codex/events/codex-session-source.js +2 -0
- package/dist/adapter/codex/events/hook-refusals.js +2 -0
- package/dist/adapter/codex/events/rollout-scan.js +1 -1
- package/dist/adapter/codex/hooks/codex-conversation.js +54 -0
- package/dist/adapter/codex/hooks/pre-tool-use-input.js +2 -0
- package/dist/adapter/codex/hooks/reader.js +38 -0
- package/dist/adapter/codex/hooks/stop-input.js +32 -0
- package/dist/adapter/codex/hooks/stop-refusals.js +3 -6
- package/dist/adapter/codex/probe/codex-probe.js +2 -0
- package/dist/adapter/codex/settings/codex-check.js +38 -0
- package/dist/adapter/codex/settings/codex-hooks.js +168 -1
- package/dist/adapter/codex/settings/hook-approval.js +374 -0
- package/dist/cli/command-router.js +8 -2
- package/dist/cli/commands/check-cli-command.js +2 -0
- package/dist/cli/commands/codex-stop-cli-command.js +15 -12
- package/dist/cli/commands/doctor-arguments.js +2 -0
- package/dist/cli/commands/doctor-usage.js +2 -0
- package/dist/cli/commands/init-cli-command.js +10 -5
- package/dist/cli/commands/notify-cli-command.js +3 -1
- package/dist/cli/commands/refuse-cli-command.js +9 -4
- package/dist/cli/commands/report-arguments.js +2 -0
- package/dist/cli/commands/report-usage.js +2 -0
- package/dist/cli/commands/sessions-cli-command.js +9 -1
- package/dist/cli/commands/start-cli-command.js +29 -2
- package/dist/cli/commands/watch-cli-command.js +4 -2
- package/dist/cli/exit-codes.js +2 -0
- package/dist/cli.js +12 -1
- package/dist/composition-root.js +101 -16
- package/dist/core/access/command-line.js +48 -2
- package/dist/core/access/listing.js +2 -0
- package/dist/core/access/path-tokens.js +2 -0
- package/dist/core/access/protected-access.js +36 -3
- package/dist/core/access/protected-values.js +17 -0
- package/dist/core/access/search-output.js +2 -0
- package/dist/core/access/search-reach.js +2 -0
- package/dist/core/combined-project-catalogue.js +7 -1
- package/dist/core/correlation/correlate.js +13 -1
- package/dist/core/detector/prompt-signals.js +2 -0
- package/dist/core/guarded-places.js +42 -0
- package/dist/core/nearest-project.js +5 -2
- package/dist/core/policy/glob.js +2 -0
- package/dist/core/policy/parse-policy.js +2 -0
- package/dist/core/policy/policy.js +14 -2
- package/dist/core/policy/resolve-policy.js +2 -0
- package/dist/core/redaction/entropy.js +2 -0
- package/dist/core/redaction/redactor.js +2 -0
- package/dist/core/redaction/value-shapes.js +2 -0
- package/dist/core/redaction/value-trace.js +2 -0
- package/dist/core/session-catalogue.js +7 -0
- package/dist/core/session-filter.js +2 -0
- package/dist/infrastructure/clack-asker.js +2 -0
- package/dist/infrastructure/clack-chooser.js +2 -0
- package/dist/infrastructure/clack-multi-chooser.js +2 -0
- package/dist/infrastructure/file-alert-store.js +10 -3
- package/dist/infrastructure/file-checked-store.js +2 -0
- package/dist/infrastructure/file-mark-store.js +2 -0
- package/dist/infrastructure/file-onboarding-store.js +2 -0
- package/dist/infrastructure/file-page-servers.js +51 -0
- package/dist/infrastructure/folder-window.js +2 -0
- package/dist/infrastructure/node-agentwhy-invocation.js +2 -0
- package/dist/infrastructure/node-background-run.js +45 -0
- package/dist/infrastructure/node-browser.js +2 -0
- package/dist/infrastructure/node-file-system.js +42 -1
- package/dist/infrastructure/node-local-server.js +2 -0
- package/dist/infrastructure/node-page-probe.js +40 -0
- package/dist/infrastructure/os-notifier.js +2 -0
- package/dist/infrastructure/real-directory.js +2 -0
- package/dist/infrastructure/shell-command-trial.js +60 -0
- package/dist/infrastructure/terminal-banner.js +2 -0
- package/dist/ports/command-trial.js +1 -0
- package/dist/ports/directory-reader.js +14 -1
- package/dist/ports/file-replacer.js +1 -0
- package/dist/ports/mark-store.js +2 -0
- package/dist/ports/page-server.js +1 -0
- package/dist/refuse/command-reach.js +2 -0
- package/dist/refuse/command-refusal.js +19 -30
- package/dist/refuse/render/refusal-words.js +3 -1
- package/dist/report/build-report.js +59 -11
- package/dist/report/check/actions-digest.js +3 -0
- package/dist/report/check/check-lines.js +2 -1
- package/dist/report/check/render/text-digest-renderer.js +31 -9
- package/dist/report/check/session-actions.js +4 -6
- package/dist/report/check/session-check.js +8 -0
- package/dist/report/choose-policy.js +7 -1
- package/dist/report/nothing-saved-here.js +16 -0
- package/dist/report/private-files/tell-lists.js +2 -0
- package/dist/report/project-rules.js +2 -0
- package/dist/report/refusals.js +15 -0
- package/dist/report/render/home-relative.js +2 -0
- package/dist/report/render/html-head.js +2 -0
- package/dist/report/render/html-report-styles.js +2 -0
- package/dist/report/render/html-sidebar.js +2 -0
- package/dist/report/render/path-tail.js +2 -0
- package/dist/report/render/report-copy.js +2 -0
- package/dist/report/render/report-page/advanced-view.js +2 -0
- package/dist/report/render/report-page/files-view.js +2 -0
- package/dist/report/render/report-page/files.js +2 -0
- package/dist/report/render/report-page/fix-wizard-script.js +2 -0
- package/dist/report/render/report-page/helpers-view.js +2 -0
- package/dist/report/render/report-page/item-names.js +2 -0
- package/dist/report/render/report-page/protect-patterns.js +2 -0
- package/dist/report/render/report-page/report-views.js +2 -0
- package/dist/report/render/report-page/to-do-view.js +3 -2
- package/dist/report/render/text-report-renderer.js +2 -0
- package/dist/report/render/ui/add-file-popup.js +2 -0
- package/dist/report/render/ui/app-sidebar.js +2 -0
- package/dist/report/render/ui/ask-panel.js +2 -0
- package/dist/report/render/ui/avatar.js +2 -0
- package/dist/report/render/ui/backdrop.js +2 -0
- package/dist/report/render/ui/brand-mark.js +2 -0
- package/dist/report/render/ui/checklist.js +2 -0
- package/dist/report/render/ui/confirm-dialog.js +2 -0
- package/dist/report/render/ui/drawer.js +2 -0
- package/dist/report/render/ui/file-chip.js +2 -0
- package/dist/report/render/ui/fold-line.js +2 -0
- package/dist/report/render/ui/guide-card.js +2 -0
- package/dist/report/render/ui/live-script.js +2 -0
- package/dist/report/render/ui/page-shell.js +2 -0
- package/dist/report/render/ui/popup.js +2 -0
- package/dist/report/render/ui/progress.js +2 -0
- package/dist/report/render/ui/project-list.js +9 -9
- package/dist/report/render/ui/stats.js +2 -0
- package/dist/report/render/ui/status-icon.js +2 -0
- package/dist/report/render/ui/status-look.js +2 -0
- package/dist/report/render/ui/task-list.js +2 -0
- package/dist/report/render/ui/tokens.js +2 -0
- package/dist/report/render/ui/update-notice.js +2 -0
- package/dist/report/render/ui/words/app-words.js +3 -3
- package/dist/report/render/ui/words/onboarding-words.js +9 -9
- package/dist/report/render/ui/words/projects-words.js +3 -3
- package/dist/report/render/ui/words/report-words.js +11 -11
- package/dist/report/render/ui/words/settings-words.js +30 -6
- package/dist/report/rule-names.js +2 -0
- package/dist/report/session-report.js +2 -0
- package/dist/report/start/conversations/calendar-view.js +2 -0
- package/dist/report/start/conversations/conversation-columns.js +2 -0
- package/dist/report/start/conversations/empty-week.js +2 -0
- package/dist/report/start/conversations/files-window.js +2 -0
- package/dist/report/start/conversations/period-section.js +2 -0
- package/dist/report/start/conversations/periods-script.js +2 -0
- package/dist/report/start/conversations/periods.js +2 -0
- package/dist/report/start/conversations/week-view.js +2 -0
- package/dist/report/start/conversations/weeks.js +2 -0
- package/dist/report/start/detached-start.js +94 -0
- package/dist/report/start/month/month-view.js +2 -0
- package/dist/report/start/onboarding/done.js +9 -3
- package/dist/report/start/onboarding/finish-onboarding.js +6 -4
- package/dist/report/start/onboarding/intro.js +2 -0
- package/dist/report/start/onboarding/onboarding-renderer.js +4 -4
- package/dist/report/start/onboarding/onboarding-script.js +2 -0
- package/dist/report/start/onboarding/onboarding-view.js +1 -1
- package/dist/report/start/onboarding/project-step.js +1 -1
- package/dist/report/start/onboarding/steps.js +2 -0
- package/dist/report/start/onboarding/welcome-decision.js +44 -0
- package/dist/report/start/onboarding/welcome.js +2 -0
- package/dist/report/start/page-notice.js +2 -0
- package/dist/report/start/projects/index-projects.js +5 -2
- package/dist/report/start/projects/moved-page.js +2 -0
- package/dist/report/start/projects/project-switch.js +2 -0
- package/dist/report/start/projects/projects-window.js +2 -0
- package/dist/report/start/projects/temporary-space.js +2 -0
- package/dist/report/start/render/start-words.js +21 -2
- package/dist/report/start/repository.js +2 -0
- package/dist/report/start/serve/index-handler.js +4 -1
- package/dist/report/start/serve/settings-setup.js +10 -0
- package/dist/report/start/session-start.js +80 -55
- package/dist/report/start/settings/alerts-tab.js +4 -1
- package/dist/report/start/settings/files-tab.js +18 -12
- package/dist/report/start/settings/general-tab.js +33 -1
- package/dist/report/start/settings/settings-renderer.js +2 -0
- package/dist/report/start/settings/settings-script.js +8 -1
- package/dist/report/start/settings/settings-view.js +29 -2
- package/dist/report/start/settings/settings-windows.js +16 -5
- package/dist/report/start/settings-files.js +2 -0
- package/dist/report/start/to-fix/done-list.js +2 -0
- package/dist/report/start/to-fix/file-window.js +2 -0
- package/dist/report/start/to-fix/to-fix-list.js +2 -0
- package/dist/report/start/to-fix/to-fix-renderer.js +2 -0
- package/dist/report/start/to-fix/to-fix-script.js +2 -0
- package/dist/report/watch/agent-alert.js +3 -0
- package/dist/report/watch/codex-turn-format.js +69 -0
- package/dist/report/watch/notice-choices.js +2 -0
- package/dist/report/watch/notice-settings.js +2 -0
- package/dist/report/watch/preferences.js +2 -0
- package/dist/report/watch/render/codex-request-words.js +106 -0
- package/dist/report/watch/render/instruction-words.js +114 -12
- package/dist/report/watch/render/notice-words.js +74 -4
- package/dist/report/watch/subagent-watch.js +270 -45
- package/dist/setup/behind.js +2 -0
- package/dist/setup/codex-mirror.js +311 -104
- package/dist/setup/not-a-project.js +2 -0
- package/dist/setup/project-setup.js +2 -0
- package/dist/shared/compare.js +2 -0
- package/dist/shared/content-version.js +2 -0
- package/dist/shared/counter.js +2 -0
- package/dist/shared/file-stamp.js +2 -0
- package/dist/shared/label.js +2 -0
- package/dist/shared/package-name.js +2 -0
- package/dist/shared/plain-invocation.js +2 -0
- package/dist/shared/printable.js +2 -0
- package/dist/shared/sentence.js +2 -0
- package/dist/shared/terminal-logo.js +2 -0
- package/dist/shared/text-table.js +2 -0
- package/dist/shared/wrap.js +2 -0
- package/package.json +2 -1
- package/dist/refuse/render/codex-stop-words.js +0 -21
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { HOOK_REFUSAL } from '../contract/hook-refusals.js';
|
|
2
4
|
/**
|
|
3
5
|
* agentwhy's own reason (`refuse/render/refusal-words.ts` `refusalReason`), after Codex's words for a hook's refusal: "it
|
|
@@ -492,7 +492,7 @@ class FileState {
|
|
|
492
492
|
},
|
|
493
493
|
result: {
|
|
494
494
|
callId: id, content: refusal.reason, stage: 'model', completeness: 'complete', evidence: output.evidence,
|
|
495
|
-
denial: { kind: HOOK_REFUSAL.denialKind, recognised: true },
|
|
495
|
+
denial: { kind: HOOK_REFUSAL.denialKind, recognised: true, source: 'rule' },
|
|
496
496
|
},
|
|
497
497
|
});
|
|
498
498
|
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { FileAccessError } from '../../../ports/file-access-error.js';
|
|
4
|
+
import { isJsonObject, parseJsonObject } from '../../../shared/json.js';
|
|
5
|
+
import { THREAD_NAMES } from '../contract/session.js';
|
|
6
|
+
/**
|
|
7
|
+
* The one conversation a Codex session belongs to (`codex-says-it-too` CXB5, CX8). Measured 2026-10-02: the Codex apps
|
|
8
|
+
* give one visible conversation a new session id as it goes on - four of five conversations of that day's test project
|
|
9
|
+
* held two ids each - so anything said "once a conversation" and keyed by the session id was said again, and a line
|
|
10
|
+
* already said in one app came again in the other. The only record joining them is Codex's own thread index: rows of
|
|
11
|
+
* `{id, thread_name, updated_at}`, one thread's sessions sharing its name. The conversation's key is the thread's
|
|
12
|
+
* earliest session id - an id, never the name, so nothing a person typed reaches a file name built from this.
|
|
13
|
+
*
|
|
14
|
+
* Where the index has no row for the session (an `exec` run, a build that keeps no index) or cannot be read, the
|
|
15
|
+
* session id stands, as before. Two conversations Codex happened to title alike are grouped as one - the cost is a
|
|
16
|
+
* once-line not repeated across the twins, never a finding lost.
|
|
17
|
+
*/
|
|
18
|
+
export async function codexConversationOf(files, indexPath, sessionId) {
|
|
19
|
+
const name = new Map();
|
|
20
|
+
const firstSeen = new Map();
|
|
21
|
+
try {
|
|
22
|
+
for await (const raw of files.readLines(indexPath)) {
|
|
23
|
+
const row = parseJsonObject(raw);
|
|
24
|
+
if (!isJsonObject(row))
|
|
25
|
+
continue;
|
|
26
|
+
const id = row[THREAD_NAMES.id];
|
|
27
|
+
const threadName = row[THREAD_NAMES.name];
|
|
28
|
+
const at = row[THREAD_NAMES.updatedAt];
|
|
29
|
+
if (typeof id !== 'string' || id === '' || typeof threadName !== 'string')
|
|
30
|
+
continue;
|
|
31
|
+
// A later line renames the thread: the name an id has is its last line's (as the titles reader reads it).
|
|
32
|
+
name.set(id, threadName);
|
|
33
|
+
if (!firstSeen.has(id) && typeof at === 'string')
|
|
34
|
+
firstSeen.set(id, at);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error instanceof FileAccessError)
|
|
39
|
+
return sessionId;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
const thread = name.get(sessionId);
|
|
43
|
+
if (thread === undefined)
|
|
44
|
+
return sessionId;
|
|
45
|
+
let earliest = sessionId;
|
|
46
|
+
for (const [id, current] of name) {
|
|
47
|
+
if (current !== thread)
|
|
48
|
+
continue;
|
|
49
|
+
const order = (one) => `${firstSeen.get(one) ?? ''}\u0000${one}`;
|
|
50
|
+
if (order(id) < order(earliest))
|
|
51
|
+
earliest = id;
|
|
52
|
+
}
|
|
53
|
+
return earliest;
|
|
54
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { FileAccessError } from '../../../ports/file-access-error.js';
|
|
4
|
+
import { isJsonObject, parseJsonObject } from '../../../shared/json.js';
|
|
5
|
+
import { READER, SESSION } from '../contract/session.js';
|
|
6
|
+
const NOBODY = { attended: false, quietSaidByAgent: false, foldsTurn: false };
|
|
7
|
+
/**
|
|
8
|
+
* Read from the rollout's first line, the one line it is read for. A rollout that cannot be read, or a first line that
|
|
9
|
+
* is not Codex's, is nobody reading: the agent is then never asked to speak, and the line is said all the same.
|
|
10
|
+
*/
|
|
11
|
+
export async function codexReaderOf(files, rollout) {
|
|
12
|
+
if (rollout === undefined)
|
|
13
|
+
return NOBODY;
|
|
14
|
+
const lines = files.readLines(rollout)[Symbol.asyncIterator]();
|
|
15
|
+
try {
|
|
16
|
+
const first = await lines.next();
|
|
17
|
+
const line = first.done === true ? undefined : parseJsonObject(first.value);
|
|
18
|
+
if (line?.[SESSION.lineType] !== SESSION.metadataType)
|
|
19
|
+
return NOBODY;
|
|
20
|
+
const payload = line[SESSION.payload];
|
|
21
|
+
if (!isJsonObject(payload) || payload[SESSION.source] !== READER.personSource)
|
|
22
|
+
return NOBODY;
|
|
23
|
+
const originator = payload[SESSION.originator];
|
|
24
|
+
return {
|
|
25
|
+
attended: true,
|
|
26
|
+
quietSaidByAgent: originator !== READER.terminalOriginator,
|
|
27
|
+
foldsTurn: typeof originator === 'string' && READER.desktopOriginators.includes(originator),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error instanceof FileAccessError)
|
|
32
|
+
return NOBODY;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await lines.return?.();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { STOP } from '../contract/hooks.js';
|
|
4
|
+
import { parseJsonObject } from '../../../shared/json.js';
|
|
5
|
+
/** Codex's `Stop` input, read by the names Codex gives it. Never throws: what cannot be used is named. */
|
|
6
|
+
export function parseCodexStopInput(text) {
|
|
7
|
+
const input = parseJsonObject(text);
|
|
8
|
+
if (input === undefined)
|
|
9
|
+
return { unusable: 'not-json' };
|
|
10
|
+
const { fields } = STOP;
|
|
11
|
+
if (input[fields.event] !== STOP.event)
|
|
12
|
+
return { unusable: 'another-event' };
|
|
13
|
+
const sessionId = input[fields.sessionId];
|
|
14
|
+
if (typeof sessionId !== 'string' || sessionId === '')
|
|
15
|
+
return { unusable: 'field-missing' };
|
|
16
|
+
const transcriptPath = input[fields.transcriptPath];
|
|
17
|
+
const lastMessage = input[fields.lastMessage];
|
|
18
|
+
return {
|
|
19
|
+
turn: {
|
|
20
|
+
sessionId,
|
|
21
|
+
// As for Claude Code: anything but `true` is no continuation, and the store refuses a second block anyway.
|
|
22
|
+
active: input[fields.active] === true,
|
|
23
|
+
...(typeof transcriptPath === 'string' && transcriptPath !== '' ? { transcriptPath } : {}),
|
|
24
|
+
...(typeof lastMessage === 'string' ? { lastMessage } : {}),
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** The folder a Codex hook ran in: where the project's rules are looked for (CK3). */
|
|
29
|
+
export function codexStopFolder(text) {
|
|
30
|
+
const folder = parseJsonObject(text)?.[STOP.fields.workingDirectory];
|
|
31
|
+
return typeof folder === 'string' && folder !== '' ? folder : undefined;
|
|
32
|
+
}
|
|
@@ -12,15 +12,12 @@ import { hookRefusalsIn } from '../events/hook-refusals.js';
|
|
|
12
12
|
* outputs with the Stop input's turn id. It never returns any other transcript content.
|
|
13
13
|
*/
|
|
14
14
|
export class CodexTurnRefusals {
|
|
15
|
-
#input;
|
|
16
15
|
#files;
|
|
17
|
-
constructor(
|
|
18
|
-
this.#input = input;
|
|
16
|
+
constructor(files) {
|
|
19
17
|
this.#files = files;
|
|
20
18
|
}
|
|
21
|
-
async find() {
|
|
22
|
-
const
|
|
23
|
-
const input = text === undefined ? undefined : parseJsonObject(text);
|
|
19
|
+
async find(text) {
|
|
20
|
+
const input = parseJsonObject(text);
|
|
24
21
|
if (input === undefined || input[STOP.fields.event] !== STOP.event || input[STOP.fields.active] === true)
|
|
25
22
|
return [];
|
|
26
23
|
const turnId = input[STOP.fields.turnId];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { FileAccessError } from '../../../ports/file-access-error.js';
|
|
5
|
+
import { parseJsonObject } from '../../../shared/json.js';
|
|
6
|
+
import { USER_HOOKS } from '../contract/hooks.js';
|
|
7
|
+
import { approvedCodexEntries } from './codex-hooks.js';
|
|
8
|
+
import { approvalKey, approvalOf, entryHash } from './hook-approval.js';
|
|
9
|
+
export async function codexCheckState(files, home) {
|
|
10
|
+
const userPath = join(home, USER_HOOKS.directory, USER_HOOKS.file);
|
|
11
|
+
const file = parseJsonObject((await textOf(files, userPath)) ?? '');
|
|
12
|
+
if (file === undefined)
|
|
13
|
+
return 'absent';
|
|
14
|
+
// The entries setup approves, not every entry of agentwhy's: one left sharing a group with another tool's (AO4) is
|
|
15
|
+
// never approved, so demanding an approval for it too would pin the state at `stale` with no repair to reach.
|
|
16
|
+
const placed = approvedCodexEntries(file);
|
|
17
|
+
if (placed.length === 0)
|
|
18
|
+
return 'absent';
|
|
19
|
+
const config = await textOf(files, join(home, USER_HOOKS.directory, USER_HOOKS.config));
|
|
20
|
+
if (config === undefined)
|
|
21
|
+
return 'stale';
|
|
22
|
+
const verified = placed.every((entry) => {
|
|
23
|
+
const hash = entryHash(entry);
|
|
24
|
+
const approval = approvalOf(config, approvalKey(userPath, entry));
|
|
25
|
+
return hash !== undefined && approval !== undefined && approval.hash === hash && !approval.disabled;
|
|
26
|
+
});
|
|
27
|
+
return verified ? 'on' : 'stale';
|
|
28
|
+
}
|
|
29
|
+
async function textOf(files, path) {
|
|
30
|
+
try {
|
|
31
|
+
return await files.readText(path);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error instanceof FileAccessError)
|
|
35
|
+
return undefined;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { isJsonObject } from '../../../shared/json.js';
|
|
2
|
-
import { PRE_TOOL_USE, PROJECT_HOOKS } from '../contract/hooks.js';
|
|
4
|
+
import { HOOK_HASH, PRE_TOOL_USE, PROJECT_HOOKS, USER_HOOKS } from '../contract/hooks.js';
|
|
3
5
|
/**
|
|
4
6
|
* agentwhy's Codex hook, and no one else's: `refuse` with `--codex` straight after it. Nothing but agentwhy writes that
|
|
5
7
|
* flag, so a command with it is taken out and replaced, and every other command in the file is left as it was (K13).
|
|
@@ -14,6 +16,16 @@ const RUNS_CODEX_STOP = /(?:^|\s)codex-stop\s+--codex(?:\s|$)/;
|
|
|
14
16
|
export function codexRefuseCommand(invoke, settings) {
|
|
15
17
|
return `${invoke} refuse --codex${settings === undefined ? '' : ` --settings "${settings}"`}`;
|
|
16
18
|
}
|
|
19
|
+
/** The `--settings` a command `codexRefuseCommand` wrote names, as written - relative to the project. */
|
|
20
|
+
const SETTINGS_NAMED = /\s--settings\s+"([^"]*)"/;
|
|
21
|
+
/**
|
|
22
|
+
* The settings file agentwhy's Codex `refuse` reads in this file, relative to the project, where it names one (CK2):
|
|
23
|
+
* the rules Codex's Stop hook reads too (`codex-says-it-too` CX5), so the two never read different lists.
|
|
24
|
+
*/
|
|
25
|
+
export function codexRefuseSettings(file) {
|
|
26
|
+
const command = codexRefuseIn(file);
|
|
27
|
+
return command === undefined ? undefined : SETTINGS_NAMED.exec(command)?.[1];
|
|
28
|
+
}
|
|
17
29
|
/** The companion Stop hook asks Codex to credit agentwhy for a blocked command in its reply. */
|
|
18
30
|
export function codexStopCommand(invoke) {
|
|
19
31
|
return `${invoke} codex-stop --codex`;
|
|
@@ -71,6 +83,161 @@ export function withoutCodexRefuse(file) {
|
|
|
71
83
|
}
|
|
72
84
|
return { file: removed === 0 ? file : { ...file, [PROJECT_HOOKS.hooks]: kept }, removed };
|
|
73
85
|
}
|
|
86
|
+
/** The two events agentwhy writes, each with the pattern that tells its command and the group it writes. */
|
|
87
|
+
const OWN_EVENTS = [
|
|
88
|
+
{ event: 'PreToolUse', runs: RUNS_CODEX_REFUSE, matcher: PRE_TOOL_USE.shellTool },
|
|
89
|
+
{ event: 'Stop', runs: RUNS_CODEX_STOP },
|
|
90
|
+
];
|
|
91
|
+
/**
|
|
92
|
+
* agentwhy's entries where they stand in this file, with the indices Codex keys their approval by
|
|
93
|
+
* (`2026-10-02-codex-approves-its-own-hook.md` AOB1, AO3). Every array item counts toward an index, valid or not,
|
|
94
|
+
* because Codex enumerates them all.
|
|
95
|
+
*/
|
|
96
|
+
export function agentwhyCodexEntries(file) {
|
|
97
|
+
const hooks = file[PROJECT_HOOKS.hooks];
|
|
98
|
+
if (!isJsonObject(hooks))
|
|
99
|
+
return [];
|
|
100
|
+
const found = [];
|
|
101
|
+
for (const { event, runs } of OWN_EVENTS) {
|
|
102
|
+
const list = hooks[event];
|
|
103
|
+
if (!Array.isArray(list))
|
|
104
|
+
continue;
|
|
105
|
+
list.forEach((group, groupIndex) => {
|
|
106
|
+
if (!isJsonObject(group) || !Array.isArray(group[PROJECT_HOOKS.commands]))
|
|
107
|
+
return;
|
|
108
|
+
group[PROJECT_HOOKS.commands].forEach((item, handlerIndex) => {
|
|
109
|
+
if (!isJsonObject(item) || typeof item[PROJECT_HOOKS.command] !== 'string' || !runs.test(item[PROJECT_HOOKS.command]))
|
|
110
|
+
return;
|
|
111
|
+
const matcher = group[PROJECT_HOOKS.matcher];
|
|
112
|
+
found.push({ event, group: groupIndex, handler: handlerIndex, ...(typeof matcher === 'string' ? { matcher } : {}), entry: item });
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return found;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The entries agentwhy approves: the first of each event, which is the one `withOwnPair` keeps and setup files an
|
|
120
|
+
* approval for (AO3). A further entry of the same event is one that shares a group with another tool's and stays
|
|
121
|
+
* (AO4), unapproved and so skipped by Codex - never a reason to call the approved check anything but what it is.
|
|
122
|
+
*/
|
|
123
|
+
export function approvedCodexEntries(file) {
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
return agentwhyCodexEntries(file).filter((entry) => {
|
|
126
|
+
if (seen.has(entry.event))
|
|
127
|
+
return false;
|
|
128
|
+
seen.add(entry.event);
|
|
129
|
+
return true;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The file holding exactly one agentwhy pair (AO1, AO4): of each event, the first agentwhy entry is rewritten **in
|
|
134
|
+
* place** to the wanted command - alone in its group, the group's matcher becomes agentwhy's own - and every other
|
|
135
|
+
* agentwhy entry of that event comes out; where there is none, a group is appended at the end of the event's list.
|
|
136
|
+
* Nothing else moves, so no other tool's approval key shifts (AOD3).
|
|
137
|
+
*/
|
|
138
|
+
export function withOwnPair(file, refuseCommand, stopCommand) {
|
|
139
|
+
const hooks = isJsonObject(file[PROJECT_HOOKS.hooks]) ? file[PROJECT_HOOKS.hooks] : {};
|
|
140
|
+
const next = { ...hooks };
|
|
141
|
+
let removed = 0;
|
|
142
|
+
let stuck = 0;
|
|
143
|
+
for (const { event, runs, matcher } of OWN_EVENTS) {
|
|
144
|
+
const command = event === 'PreToolUse' ? refuseCommand : stopCommand;
|
|
145
|
+
const list = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
|
|
146
|
+
const placed = positionsIn(list, runs);
|
|
147
|
+
const [first, ...duplicates] = placed;
|
|
148
|
+
if (first === undefined) {
|
|
149
|
+
list.push({ ...(matcher === undefined ? {} : { [PROJECT_HOOKS.matcher]: matcher }), [PROJECT_HOOKS.commands]: [ownEntry(command)] });
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const group = list[first.group];
|
|
153
|
+
const handlers = [...group[PROJECT_HOOKS.commands]];
|
|
154
|
+
handlers[first.handler] = ownEntry(command);
|
|
155
|
+
const alone = handlers.length === 1;
|
|
156
|
+
list[first.group] = { ...group, ...(alone && matcher !== undefined ? { [PROJECT_HOOKS.matcher]: matcher } : {}), [PROJECT_HOOKS.commands]: handlers };
|
|
157
|
+
}
|
|
158
|
+
const result = withoutPositions(list, duplicates);
|
|
159
|
+
removed += result.removed;
|
|
160
|
+
stuck += result.stuck;
|
|
161
|
+
next[event] = result.list;
|
|
162
|
+
}
|
|
163
|
+
return { file: { ...file, [PROJECT_HOOKS.hooks]: next }, removed, stuck };
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The file without agentwhy's entries (AO4, AO7): each comes out of its group; a group left empty is dropped only when
|
|
167
|
+
* it is the last of its event's list - and then the next one before it, while it is agentwhy's emptied group too - and
|
|
168
|
+
* otherwise stays as an empty group, which Codex skips and still counts. An entry with another tool's entry after it in
|
|
169
|
+
* the same group stays, and is counted as `stuck`: taking it out would shift that entry's key.
|
|
170
|
+
*/
|
|
171
|
+
export function withoutOwnEntries(file) {
|
|
172
|
+
const hooks = file[PROJECT_HOOKS.hooks];
|
|
173
|
+
if (!isJsonObject(hooks))
|
|
174
|
+
return { file, removed: 0, stuck: 0 };
|
|
175
|
+
const next = { ...hooks };
|
|
176
|
+
let removed = 0;
|
|
177
|
+
let stuck = 0;
|
|
178
|
+
for (const { event, runs } of OWN_EVENTS) {
|
|
179
|
+
const list = hooks[event];
|
|
180
|
+
if (!Array.isArray(list))
|
|
181
|
+
continue;
|
|
182
|
+
const result = withoutPositions([...list], positionsIn(list, runs));
|
|
183
|
+
removed += result.removed;
|
|
184
|
+
stuck += result.stuck;
|
|
185
|
+
if (result.removed === 0)
|
|
186
|
+
continue;
|
|
187
|
+
if (result.list.length === 0)
|
|
188
|
+
delete next[event];
|
|
189
|
+
else
|
|
190
|
+
next[event] = result.list;
|
|
191
|
+
}
|
|
192
|
+
return { file: removed === 0 ? file : { ...file, [PROJECT_HOOKS.hooks]: next }, removed, stuck };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* agentwhy's entry as written: the command and Codex's wait for it (AO14, AOD9), and nothing else, so its hash stays
|
|
196
|
+
* within what was measured (AO2, AOB10).
|
|
197
|
+
*/
|
|
198
|
+
function ownEntry(command) {
|
|
199
|
+
return { [PROJECT_HOOKS.type]: PROJECT_HOOKS.commandType, [PROJECT_HOOKS.command]: command, [HOOK_HASH.fields.timeout]: USER_HOOKS.timeoutSeconds };
|
|
200
|
+
}
|
|
201
|
+
function positionsIn(list, runs) {
|
|
202
|
+
const found = [];
|
|
203
|
+
list.forEach((group, groupIndex) => {
|
|
204
|
+
if (!isJsonObject(group) || !Array.isArray(group[PROJECT_HOOKS.commands]))
|
|
205
|
+
return;
|
|
206
|
+
group[PROJECT_HOOKS.commands].forEach((item, handlerIndex) => {
|
|
207
|
+
if (isJsonObject(item) && typeof item[PROJECT_HOOKS.command] === 'string' && runs.test(item[PROJECT_HOOKS.command])) {
|
|
208
|
+
found.push({ group: groupIndex, handler: handlerIndex });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
return found;
|
|
213
|
+
}
|
|
214
|
+
/** The list with these entries out, under AO4's rule that no other entry's position moves. */
|
|
215
|
+
function withoutPositions(list, positions) {
|
|
216
|
+
let removed = 0;
|
|
217
|
+
let stuck = 0;
|
|
218
|
+
const emptied = new Set();
|
|
219
|
+
const byGroup = new Map();
|
|
220
|
+
for (const { group, handler } of positions)
|
|
221
|
+
byGroup.set(group, [...(byGroup.get(group) ?? []), handler]);
|
|
222
|
+
for (const [groupIndex, handlers] of byGroup) {
|
|
223
|
+
const group = list[groupIndex];
|
|
224
|
+
const items = [...group[PROJECT_HOOKS.commands]];
|
|
225
|
+
const out = new Set(handlers);
|
|
226
|
+
// From the end: an entry comes out only while everything after it in the group comes out too.
|
|
227
|
+
let keepFrom = items.length;
|
|
228
|
+
while (keepFrom > 0 && out.has(keepFrom - 1))
|
|
229
|
+
keepFrom -= 1;
|
|
230
|
+
removed += items.length - keepFrom;
|
|
231
|
+
stuck += handlers.length - (items.length - keepFrom);
|
|
232
|
+
const kept = items.slice(0, keepFrom);
|
|
233
|
+
list[groupIndex] = { ...group, [PROJECT_HOOKS.commands]: kept };
|
|
234
|
+
if (kept.length === 0)
|
|
235
|
+
emptied.add(groupIndex);
|
|
236
|
+
}
|
|
237
|
+
while (list.length > 0 && emptied.has(list.length - 1))
|
|
238
|
+
list.pop();
|
|
239
|
+
return { list, removed, stuck };
|
|
240
|
+
}
|
|
74
241
|
function commandsOf(file, event) {
|
|
75
242
|
const hooks = file[PROJECT_HOOKS.hooks];
|
|
76
243
|
if (!isJsonObject(hooks))
|