@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,20 +1,29 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { join } from 'node:path';
|
|
2
4
|
import { SETTINGS_FILES } from '../adapter/claude-code/contract/settings.js';
|
|
3
|
-
import { installedHooks
|
|
4
|
-
import { PROJECT_HOOKS } from '../adapter/codex/contract/hooks.js';
|
|
5
|
-
import { codexRefuseCommand, codexRefuseIn, codexStopCommand,
|
|
5
|
+
import { installedHooks } from '../adapter/claude-code/settings/hook-entries.js';
|
|
6
|
+
import { PROJECT_HOOKS, USER_HOOKS } from '../adapter/codex/contract/hooks.js';
|
|
7
|
+
import { agentwhyCodexEntries, approvedCodexEntries, codexRefuseCommand, codexRefuseIn, codexStopCommand, withOwnPair, withoutOwnEntries } from '../adapter/codex/settings/codex-hooks.js';
|
|
8
|
+
import { approvalKey, entryHash, tomlDefinesHooks, withOwnApprovals, withoutOwnApprovals } from '../adapter/codex/settings/hook-approval.js';
|
|
6
9
|
import { FileAccessError } from '../ports/file-access-error.js';
|
|
7
10
|
import { parseJsonObject } from '../shared/json.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
11
|
+
import { isPlainInvocation } from '../shared/plain-invocation.js';
|
|
12
|
+
/** The person's two Codex files and the project's old one, as they are written about. */
|
|
13
|
+
const WHERE_USER = `~/${USER_HOOKS.directory}/${USER_HOOKS.file}`;
|
|
14
|
+
const WHERE_CONFIG = `~/${USER_HOOKS.directory}/${USER_HOOKS.config}`;
|
|
15
|
+
const WHERE_PROJECT = `${PROJECT_HOOKS.directory}/${PROJECT_HOOKS.file}`;
|
|
16
|
+
/** What holds once agentwhy has approved its own check (AO1, AO2) - said wherever it is written. */
|
|
17
|
+
const FROM_THE_FIRST_MESSAGE = 'Codex blocks the same files from the first message, in the terminal and in VS Code, with nothing to approve in Codex. The check acts only in projects whose rules block files.';
|
|
18
|
+
/** AOB9's cost, said in the plan the person consents to (AO8). */
|
|
19
|
+
const COST = 'It runs before every Codex shell command and at the end of every reply on this computer: about a tenth of a second where agentwhy is run directly, a quarter through npx.';
|
|
12
20
|
/**
|
|
13
|
-
* `codex-
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
21
|
+
* `2026-10-02-codex-approves-its-own-hook.md` AO1-AO11, AO14-AO16: after the setup it wraps has written Claude Code's
|
|
22
|
+
* settings - or found nothing to change - the person's own Codex files are made to match them. Where `refuse` runs in
|
|
23
|
+
* this project, `~/.codex/hooks.json` holds exactly one agentwhy pair, tried first as Codex will run it, and
|
|
24
|
+
* `~/.codex/config.toml` approves those two entries and nothing else - never a folder. The project's old copy comes
|
|
25
|
+
* out. Only on a computer with `~/.codex`; with its own part of the plan and the same consent (AO8). A project's
|
|
26
|
+
* remove keeps the computer-wide check; `--remove --codex` takes it out (AO7).
|
|
18
27
|
*/
|
|
19
28
|
export class CodexMirror {
|
|
20
29
|
#dependencies;
|
|
@@ -22,9 +31,9 @@ export class CodexMirror {
|
|
|
22
31
|
this.#dependencies = dependencies;
|
|
23
32
|
}
|
|
24
33
|
async run(options) {
|
|
25
|
-
// `init --codex` with nothing else named: Codex's
|
|
26
|
-
if (options.codex === true &&
|
|
27
|
-
return (await this.#mirror(options)) ?? { outcome: 'unchanged', output:
|
|
34
|
+
// `init --codex` with nothing else named: Codex's part alone. A remove keeps its usual course first (AO7's "too").
|
|
35
|
+
if (options.codex === true && !options.remove && nothingElse(options))
|
|
36
|
+
return (await this.#mirror(options)) ?? { outcome: 'unchanged', output: await this.#nothingFor() };
|
|
28
37
|
const result = await this.#dependencies.setup.run(options);
|
|
29
38
|
if (result.outcome !== 'written' && result.outcome !== 'unchanged')
|
|
30
39
|
return result;
|
|
@@ -34,101 +43,262 @@ export class CodexMirror {
|
|
|
34
43
|
const outcome = result.outcome === 'unchanged' && codex.outcome === 'written' ? 'written' : result.outcome;
|
|
35
44
|
return { outcome, output: `${result.output}${result.output.endsWith('\n') || result.output === '' ? '' : '\n'}\n${codex.output}` };
|
|
36
45
|
}
|
|
37
|
-
/** The change to
|
|
46
|
+
/** The change to the person's Codex files and the project's old one; `undefined` where there is none to make. */
|
|
38
47
|
async #mirror(options) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
outcome: '
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
48
|
+
if (!(await this.#dependencies.codexOnThisComputer())) {
|
|
49
|
+
// AO8: nothing is written and the folder is never created. Said only where Codex was asked for by name.
|
|
50
|
+
return options.codex === true ? refused(`there is no ~/${USER_HOOKS.directory} folder, so Codex is not used on this computer and nothing was written for it.`) : undefined;
|
|
51
|
+
}
|
|
52
|
+
const read = await this.#read();
|
|
53
|
+
if (typeof read === 'string')
|
|
54
|
+
return refused(read);
|
|
55
|
+
const projectCleaned = read.project.kind === 'object' ? withoutOwnEntries(read.project.file) : undefined;
|
|
56
|
+
const migrate = projectCleaned !== undefined && projectCleaned.removed > 0 ? projectCleaned.file : undefined;
|
|
57
|
+
if (options.remove && options.codex === true)
|
|
58
|
+
return this.#removal(options, read, migrate);
|
|
59
|
+
if (!(await this.#refuseRunsHere())) {
|
|
60
|
+
// Nothing for Codex to follow here. The computer-wide check stays for the other projects (AOD4): said on a remove.
|
|
61
|
+
const userFile = read.user.kind === 'object' ? read.user.file : {};
|
|
62
|
+
const stays = options.remove && codexRefuseIn(userFile) !== undefined
|
|
63
|
+
? `Codex: agentwhy's check in ${WHERE_USER} stays - other projects may block files with it, and it does nothing where none do. To take it out of Codex everywhere: agentwhy init --remove --codex`
|
|
64
|
+
: '';
|
|
65
|
+
if (migrate === undefined)
|
|
66
|
+
return stays === '' ? undefined : { outcome: 'unchanged', output: `${stays}\n` };
|
|
67
|
+
return this.#write(options, read, { project: migrate }, [`agentwhy's old entries come out of ${WHERE_PROJECT} in this project.`], [
|
|
68
|
+
`Codex: agentwhy's old entries are out of ${WHERE_PROJECT}.`,
|
|
69
|
+
...(stays === '' ? [] : [stays]),
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
return this.#install(options, read, migrate);
|
|
73
|
+
}
|
|
74
|
+
/** AO1-AO4, AO9, AO14: one approved pair in the person's file, tried first; the project's copy out. */
|
|
75
|
+
async #install(options, read, migrate) {
|
|
76
|
+
if (read.user.kind === 'broken' || read.user.kind === 'unreadable') {
|
|
77
|
+
return refused(`${WHERE_USER} ${read.user.kind === 'broken' ? 'is not a JSON object' : 'could not be read'}, so agentwhy's check was not written to it.`);
|
|
78
|
+
}
|
|
79
|
+
// AO16: Codex would load hooks from both files, and warn in every session.
|
|
80
|
+
if (read.user.kind === 'absent' && tomlDefinesHooks(read.config)) {
|
|
81
|
+
return refused(`${WHERE_CONFIG} defines Codex's hooks itself, so agentwhy did not create ${WHERE_USER} beside it - Codex would load both and warn about it. Adding agentwhy's check to ${WHERE_CONFIG} is not built.`);
|
|
82
|
+
}
|
|
83
|
+
const userFile = read.user.kind === 'object' ? read.user.file : {};
|
|
84
|
+
const invoke = await this.#invocationFor(options, userFile);
|
|
85
|
+
const refuse = codexRefuseCommand(invoke, undefined);
|
|
86
|
+
const stop = codexStopCommand(invoke);
|
|
87
|
+
const paired = withOwnPair(userFile, refuse, stop);
|
|
88
|
+
const hooksChange = JSON.stringify(paired.file) !== JSON.stringify(userFile);
|
|
89
|
+
const wanted = this.#approvalsOf(paired.file, read.userPath);
|
|
90
|
+
if (wanted === undefined)
|
|
91
|
+
return refused(`agentwhy's entries in ${WHERE_USER} hold something its approval was not measured over, so nothing was approved.`);
|
|
92
|
+
const edit = (fresh) => withOwnApprovals(fresh, read.userPath, wanted);
|
|
93
|
+
const first = edit(read.config);
|
|
94
|
+
if (first.kind === 'unsafe')
|
|
95
|
+
return refused(`${WHERE_CONFIG}: ${first.reason}, so agentwhy wrote nothing there - a line added beside it could stop Codex from starting.`);
|
|
96
|
+
const configChange = first.text !== read.config;
|
|
97
|
+
if (!hooksChange && !configChange) {
|
|
98
|
+
if (migrate === undefined)
|
|
99
|
+
return undefined;
|
|
100
|
+
return this.#write(options, read, { project: migrate }, [
|
|
101
|
+
`And in Codex, agentwhy's old entries come out of ${WHERE_PROJECT} in this project: your own ${WHERE_USER} already runs the check, approved. Everything else in the file stays.`,
|
|
102
|
+
], [`Codex: agentwhy's old entries are out of ${WHERE_PROJECT}.`]);
|
|
103
|
+
}
|
|
104
|
+
// AO14: a new or changed command is tried before anything is written.
|
|
105
|
+
if (hooksChange) {
|
|
106
|
+
const failed = await this.#tryBoth(refuse, stop);
|
|
107
|
+
if (failed !== undefined)
|
|
108
|
+
return refused(`${failed} so nothing was written - every Codex command on this computer would have shown an error. Fix it, or name how to run agentwhy with --command, and run this again.`);
|
|
109
|
+
}
|
|
63
110
|
const plan = [
|
|
64
|
-
|
|
111
|
+
'And in Codex, agentwhy will change your own Codex files, outside every project:',
|
|
112
|
+
'',
|
|
113
|
+
` In ${WHERE_USER}`,
|
|
114
|
+
` ${'before a shell command'.padEnd(24)}${refuse}`,
|
|
115
|
+
` ${'when a reply ends'.padEnd(24)}${stop}`,
|
|
116
|
+
` In ${WHERE_CONFIG}`,
|
|
117
|
+
` approve those two entries - agentwhy's own and nothing else, and no folder is trusted`,
|
|
118
|
+
...(migrate === undefined ? [] : ['', ` agentwhy's old entries come out of ${WHERE_PROJECT} in this project: the check above replaces them.`]),
|
|
119
|
+
...(paired.stuck === 0 ? [] : ['', ` ${paired.stuck} more agentwhy ${paired.stuck === 1 ? 'entry shares' : 'entries share'} a group with another tool's in ${WHERE_USER} and ${paired.stuck === 1 ? 'stays' : 'stay'}: taking ${paired.stuck === 1 ? 'it' : 'them'} out would un-approve that tool. Take ${paired.stuck === 1 ? 'it' : 'them'} out by hand.`]),
|
|
65
120
|
'',
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
: [' Run automatically', ` ${'before a shell command'.padEnd(24)}${wanted}`, ` ${'when a reply ends'.padEnd(24)}${stop}`]),
|
|
121
|
+
` ${FROM_THE_FIRST_MESSAGE}`,
|
|
122
|
+
` ${COST}`,
|
|
69
123
|
'',
|
|
70
|
-
|
|
71
|
-
' Everything else in the file stays. It is rewritten as two-space JSON.',
|
|
124
|
+
' Everything else in these files stays. The hooks file is rewritten as two-space JSON.',
|
|
72
125
|
];
|
|
126
|
+
const userLine = read.user.kind === 'absent' || codexRefuseIn(userFile) === undefined
|
|
127
|
+
? `Codex: agentwhy's check is in ${WHERE_USER}, approved in ${WHERE_CONFIG} - its own entries and nothing else. ${FROM_THE_FIRST_MESSAGE}`
|
|
128
|
+
: hooksChange
|
|
129
|
+
? `Codex: agentwhy's check in ${WHERE_USER} changed, and agentwhy approved it again (${WHERE_CONFIG}).`
|
|
130
|
+
: configChange
|
|
131
|
+
? `Codex: agentwhy's approval in ${WHERE_CONFIG} was brought up to date with ${WHERE_USER}.`
|
|
132
|
+
: undefined;
|
|
133
|
+
return this.#write(options, read, {
|
|
134
|
+
...(configChange ? { config: { before: read.config, edit } } : {}),
|
|
135
|
+
...(hooksChange ? { hooks: paired.file } : {}),
|
|
136
|
+
...(migrate === undefined ? {} : { project: migrate }),
|
|
137
|
+
}, plan, [
|
|
138
|
+
...(userLine === undefined ? [] : [userLine]),
|
|
139
|
+
...(migrate === undefined ? [] : [`Codex: agentwhy's old entries are out of ${WHERE_PROJECT}.`]),
|
|
140
|
+
]);
|
|
141
|
+
}
|
|
142
|
+
/** AO7: `--remove --codex` - agentwhy's entries and their approvals out of the person's two files, and the project's copy. */
|
|
143
|
+
async #removal(options, read, migrate) {
|
|
144
|
+
if (read.user.kind === 'broken' || read.user.kind === 'unreadable') {
|
|
145
|
+
return refused(`${WHERE_USER} ${read.user.kind === 'broken' ? 'is not a JSON object' : 'could not be read'}, so agentwhy's check was not taken out of it.`);
|
|
146
|
+
}
|
|
147
|
+
const userFile = read.user.kind === 'object' ? read.user.file : {};
|
|
148
|
+
const placed = agentwhyCodexEntries(userFile);
|
|
149
|
+
const keys = new Set(placed.map((entry) => approvalKey(read.userPath, entry)));
|
|
150
|
+
const hashes = new Set(placed.map((entry) => entryHash(entry)).filter((hash) => hash !== undefined));
|
|
151
|
+
const cleaned = withoutOwnEntries(userFile);
|
|
152
|
+
const edit = (fresh) => withoutOwnApprovals(fresh, read.userPath, keys, hashes);
|
|
153
|
+
const first = edit(read.config);
|
|
154
|
+
if (first.kind === 'unsafe')
|
|
155
|
+
return refused(`${WHERE_CONFIG}: ${first.reason}, so agentwhy took nothing out - take agentwhy's approval out of it by hand.`);
|
|
156
|
+
const configChange = first.text !== read.config;
|
|
157
|
+
if (cleaned.removed === 0 && !configChange && migrate === undefined) {
|
|
158
|
+
return { outcome: 'unchanged', output: `Codex: agentwhy's check is not in ${WHERE_USER}, so there was nothing to take out.\n` };
|
|
159
|
+
}
|
|
160
|
+
const plan = [
|
|
161
|
+
'And in Codex, agentwhy will change your own Codex files, outside every project:',
|
|
162
|
+
'',
|
|
163
|
+
' Stop running',
|
|
164
|
+
` agentwhy's check before every shell command, and its Codex reply message`,
|
|
165
|
+
` Take its approvals out of ${WHERE_CONFIG}`,
|
|
166
|
+
...(migrate === undefined ? [] : [` Take its old entries out of ${WHERE_PROJECT} in this project`]),
|
|
167
|
+
'',
|
|
168
|
+
' Codex then no longer blocks anything for agentwhy, in any project on this computer.',
|
|
169
|
+
'',
|
|
170
|
+
' Everything else in these files stays. The hooks file is rewritten as two-space JSON.',
|
|
171
|
+
];
|
|
172
|
+
return this.#write(options, read, {
|
|
173
|
+
...(configChange ? { config: { before: read.config, edit } } : {}),
|
|
174
|
+
...(cleaned.removed > 0 ? { hooks: cleaned.file } : {}),
|
|
175
|
+
...(migrate === undefined ? {} : { project: migrate }),
|
|
176
|
+
}, plan, [
|
|
177
|
+
`Codex: agentwhy's check and its approvals are out of ${WHERE_USER} and ${WHERE_CONFIG}. Codex no longer blocks anything for agentwhy.`,
|
|
178
|
+
...(cleaned.stuck === 0 ? [] : [`Codex: ${cleaned.stuck} agentwhy ${cleaned.stuck === 1 ? 'entry shares' : 'entries share'} a group with another tool's in ${WHERE_USER} and stayed, so as not to un-approve that tool. Take ${cleaned.stuck === 1 ? 'it' : 'them'} out by hand.`]),
|
|
179
|
+
], 'removal');
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* R7 and AO8, then every write in one order, so a failed one never leaves Codex worse off than it found it: on an
|
|
183
|
+
* install the approvals go first, so Codex never sees an entry of agentwhy's unapproved; on a removal the entries go
|
|
184
|
+
* first, for the same reason read backwards - approvals taken out from under entries that stay would make Codex ask
|
|
185
|
+
* about agentwhy's check in every session. Each half says what holds when the other one has already been written.
|
|
186
|
+
*/
|
|
187
|
+
async #write(options, read, writes, plan, done, direction = 'install') {
|
|
73
188
|
const consent = await this.#consent(options, plan);
|
|
74
189
|
if (typeof consent !== 'string')
|
|
75
190
|
return consent;
|
|
76
191
|
const said = consent === 'asked' ? '' : `${plan.join('\n')}\n\n`;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
:
|
|
192
|
+
const removing = direction === 'removal';
|
|
193
|
+
const config = async () => {
|
|
194
|
+
if (writes.config === undefined)
|
|
195
|
+
return undefined;
|
|
196
|
+
// What the other half has already done by the time this one runs, and so cannot be said to be untouched.
|
|
197
|
+
const andHooks = removing && writes.hooks !== undefined ? `agentwhy's check is out of ${WHERE_USER}, but ` : '';
|
|
198
|
+
const nothing = andHooks === '' ? 'nothing was written' : 'its approvals there could not be taken out - they name entries that are gone, so take them out by hand';
|
|
199
|
+
// AO11: Codex writes this file too, at the start of a conversation (AOB3). Read again; recomputed once if it moved.
|
|
200
|
+
const fresh = await this.#readText(read.configPath);
|
|
201
|
+
if (fresh === undefined)
|
|
202
|
+
return { outcome: 'unwritable', output: `${said}Codex: ${andHooks}${WHERE_CONFIG} could not be read again, so ${nothing}.\n` };
|
|
203
|
+
const edit = fresh === writes.config.before ? writes.config.edit(writes.config.before) : writes.config.edit(fresh);
|
|
204
|
+
if (edit.kind === 'unsafe')
|
|
205
|
+
return { outcome: 'refused', output: `${said}Codex: ${andHooks}${WHERE_CONFIG}: ${edit.reason}, so ${nothing}.\n` };
|
|
206
|
+
if (edit.text !== fresh && !(await this.#replace(read.configPath, edit.text))) {
|
|
207
|
+
return {
|
|
208
|
+
outcome: 'unwritable',
|
|
209
|
+
output: `${said}Codex: ${andHooks}${WHERE_CONFIG} could not be written, so ${andHooks === '' ? 'nothing was changed for Codex' : nothing}.\n`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
95
213
|
};
|
|
214
|
+
const hooks = async () => {
|
|
215
|
+
if (writes.hooks === undefined)
|
|
216
|
+
return undefined;
|
|
217
|
+
if (await this.#replace(read.userPath, `${JSON.stringify(writes.hooks, null, 2)}\n`))
|
|
218
|
+
return undefined;
|
|
219
|
+
// On a removal this half runs first, so nothing else has been touched and the check stands as it did.
|
|
220
|
+
const after = removing
|
|
221
|
+
? `Nothing was taken out: its approvals in ${WHERE_CONFIG} stay, so Codex still runs the approved check.`
|
|
222
|
+
: `Its approval may be in ${WHERE_CONFIG} already; the next run puts both right.`;
|
|
223
|
+
return { outcome: 'unwritable', output: `${said}Codex: ${WHERE_USER} could not be written. ${after}\n` };
|
|
224
|
+
};
|
|
225
|
+
for (const step of removing ? [hooks, config] : [config, hooks]) {
|
|
226
|
+
const failed = await step();
|
|
227
|
+
if (failed !== undefined)
|
|
228
|
+
return failed;
|
|
229
|
+
}
|
|
230
|
+
if (writes.project !== undefined && !(await this.#replace(read.projectPath, `${JSON.stringify(writes.project, null, 2)}\n`))) {
|
|
231
|
+
return { outcome: 'unwritable', output: `${said}Codex: ${WHERE_PROJECT} could not be written, so agentwhy's old entries are still in it.\n` };
|
|
232
|
+
}
|
|
233
|
+
return { outcome: 'written', output: `${said}${done.join('\n')}\n` };
|
|
96
234
|
}
|
|
97
235
|
/**
|
|
98
|
-
*
|
|
99
|
-
* `
|
|
236
|
+
* AO9: a new check runs the agentwhy doing this setup - `--command` where given. One already in the person's file
|
|
237
|
+
* is kept as it is unless `--command` names one or the run is an update, so projects set up by different releases
|
|
238
|
+
* do not rewrite each other's check on every write; `init --update` anywhere re-pins it (U7). Kept only where it
|
|
239
|
+
* names agentwhy in plain words (J4).
|
|
100
240
|
*/
|
|
101
|
-
async #
|
|
241
|
+
async #invocationFor(options, userFile) {
|
|
242
|
+
if (options.invoke !== undefined)
|
|
243
|
+
return options.invoke;
|
|
244
|
+
const mine = await this.#dependencies.invocation.find();
|
|
245
|
+
const existing = codexRefuseIn(userFile)?.replace(/\s+refuse\s+--codex(?:\s.*)?$/, '');
|
|
246
|
+
// AO9, J4: `codexRefuseIn` knows agentwhy's entry by the words `refuse --codex` anywhere in its command, so
|
|
247
|
+
// whatever stands before them was taken as the invocation - run by #tryBoth through a login shell before AO8's
|
|
248
|
+
// consent, then written as agentwhy's own entry and approved, which opens Codex's own gate (CKB5) from outside.
|
|
249
|
+
// The same test reads Claude Code's settings (`hook-entries.ts`); `--command` above is the person's own word.
|
|
250
|
+
if (existing !== undefined && !isPlainInvocation(existing))
|
|
251
|
+
return mine;
|
|
252
|
+
return existing === undefined || options.update === true ? mine : existing;
|
|
253
|
+
}
|
|
254
|
+
/** The approvals of agentwhy's pair as this file holds it: the first entry of each event, as `withOwnPair` keeps it. */
|
|
255
|
+
#approvalsOf(file, userPath) {
|
|
256
|
+
const wanted = [];
|
|
257
|
+
for (const entry of approvedCodexEntries(file)) {
|
|
258
|
+
const hash = entryHash(entry);
|
|
259
|
+
if (hash === undefined)
|
|
260
|
+
return undefined;
|
|
261
|
+
wanted.push({ key: approvalKey(userPath, entry), hash });
|
|
262
|
+
}
|
|
263
|
+
return wanted;
|
|
264
|
+
}
|
|
265
|
+
/** AO14: both commands, run once as Codex will, from a folder no project is above. The first failure, said; else none. */
|
|
266
|
+
async #tryBoth(refuse, stop) {
|
|
267
|
+
const trial = this.#dependencies.trial;
|
|
268
|
+
if (trial === undefined)
|
|
269
|
+
return 'agentwhy could not try the check before writing it,';
|
|
270
|
+
const folder = this.#dependencies.trialFolder;
|
|
271
|
+
const timeoutMs = USER_HOOKS.timeoutSeconds * 1000;
|
|
272
|
+
const inputs = [
|
|
273
|
+
[refuse, JSON.stringify({ cwd: folder, hook_event_name: 'PreToolUse', tool_name: 'Bash', tool_input: { command: 'true' } })],
|
|
274
|
+
[stop, JSON.stringify({ cwd: folder, hook_event_name: 'Stop', stop_hook_active: false })],
|
|
275
|
+
];
|
|
276
|
+
for (const [commandLine, input] of inputs) {
|
|
277
|
+
const outcome = await trial.run({ commandLine, input, folder, timeoutMs });
|
|
278
|
+
const failure = trialFailure(commandLine, outcome);
|
|
279
|
+
if (failure !== undefined)
|
|
280
|
+
return failure;
|
|
281
|
+
}
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
/** Whether Claude Code's `refuse` runs in this project after the setup: the local file first, as `init` reads them. */
|
|
285
|
+
async #refuseRunsHere() {
|
|
102
286
|
const directory = join(this.#dependencies.workingDirectory, SETTINGS_FILES.directory);
|
|
103
287
|
for (const file of [SETTINGS_FILES.local, SETTINGS_FILES.shared]) {
|
|
104
288
|
const read = await this.#readJson(join(directory, file));
|
|
105
|
-
if (read.kind
|
|
106
|
-
continue;
|
|
107
|
-
const rules = rulesReadByHooks(read.file).get('refuse') ?? 'default';
|
|
108
|
-
const invoke = runningInvocation(read.file);
|
|
109
|
-
if (rules === 'policy' || rules === 'other')
|
|
110
|
-
return { kind: 'elsewhere' };
|
|
111
|
-
return {
|
|
112
|
-
kind: 'refuse',
|
|
113
|
-
...(invoke === undefined ? {} : { invoke }),
|
|
114
|
-
...(rules === 'default' ? {} : { settings: `${SETTINGS_FILES.directory}/${SETTINGS_FILES[rules]}` }),
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
return { kind: 'none' };
|
|
118
|
-
}
|
|
119
|
-
/** CK6: a Codex conversation listed for the project, a `.codex` folder in it, or `init --codex`. */
|
|
120
|
-
async #usesCodex(options, read) {
|
|
121
|
-
if (options.codex === true || read.kind !== 'absent')
|
|
122
|
-
return true;
|
|
123
|
-
try {
|
|
124
|
-
if ((await this.#dependencies.files.kindOf(join(this.#dependencies.workingDirectory, PROJECT_HOOKS.directory))) === 'directory')
|
|
289
|
+
if (read.kind === 'object' && installedHooks(read.file).has('refuse'))
|
|
125
290
|
return true;
|
|
126
291
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
async #read() {
|
|
295
|
+
const userPath = join(this.#dependencies.home, USER_HOOKS.directory, USER_HOOKS.file);
|
|
296
|
+
const configPath = join(this.#dependencies.home, USER_HOOKS.directory, USER_HOOKS.config);
|
|
297
|
+
const projectPath = join(this.#dependencies.workingDirectory, PROJECT_HOOKS.directory, PROJECT_HOOKS.file);
|
|
298
|
+
const config = await this.#readText(configPath);
|
|
299
|
+
if (config === undefined)
|
|
300
|
+
return `${WHERE_CONFIG} could not be read, so agentwhy did not touch Codex's approvals.`;
|
|
301
|
+
return { userPath, configPath, projectPath, user: await this.#readJson(userPath), config, project: await this.#readJson(projectPath) };
|
|
132
302
|
}
|
|
133
303
|
/** R7, as `ProjectSetup` asks it: `--yes`, a person at a terminal, or the plan printed and nothing written. */
|
|
134
304
|
async #consent(options, plan) {
|
|
@@ -138,10 +308,28 @@ export class CodexMirror {
|
|
|
138
308
|
return { outcome: 'not-confirmed', output: `${plan.join('\n')}\n\nNothing was written for Codex. To write it: the same command with --yes\n` };
|
|
139
309
|
}
|
|
140
310
|
const chosen = await this.#dependencies.chooser.choose(`${plan.join('\n')}\n\nWrite it?`, [
|
|
141
|
-
{ label: 'Yes', detail:
|
|
311
|
+
{ label: 'Yes', detail: "write agentwhy's Codex files" },
|
|
142
312
|
{ label: 'No', detail: 'leave Codex as it is' },
|
|
143
313
|
]);
|
|
144
|
-
return chosen === 0 ? 'asked' : { outcome: 'declined', output:
|
|
314
|
+
return chosen === 0 ? 'asked' : { outcome: 'declined', output: 'Nothing was written for Codex.\n' };
|
|
315
|
+
}
|
|
316
|
+
/** What `init --codex` alone says where everything already matches, or there is nothing for it to follow. */
|
|
317
|
+
async #nothingFor() {
|
|
318
|
+
if (!(await this.#refuseRunsHere())) {
|
|
319
|
+
return "Codex: nothing to write. agentwhy blocks Codex where refuse runs, and refuse doesn't run here yet: agentwhy init --refuse --codex\n";
|
|
320
|
+
}
|
|
321
|
+
return `Codex: ${WHERE_USER} already runs agentwhy's check, approved. ${FROM_THE_FIRST_MESSAGE}\n`;
|
|
322
|
+
}
|
|
323
|
+
async #replace(path, text) {
|
|
324
|
+
try {
|
|
325
|
+
await this.#dependencies.files.replaceText(path, text);
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
if (!(error instanceof FileAccessError))
|
|
330
|
+
throw error;
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
145
333
|
}
|
|
146
334
|
async #readJson(path) {
|
|
147
335
|
let text;
|
|
@@ -156,16 +344,35 @@ export class CodexMirror {
|
|
|
156
344
|
const file = parseJsonObject(text);
|
|
157
345
|
return file === undefined ? { kind: 'broken' } : { kind: 'object', file };
|
|
158
346
|
}
|
|
347
|
+
/** A file's text: the empty string where there is none yet, `undefined` where there is one and it cannot be read. */
|
|
348
|
+
async #readText(path) {
|
|
349
|
+
try {
|
|
350
|
+
return await this.#dependencies.files.readText(path);
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
if (!(error instanceof FileAccessError))
|
|
354
|
+
throw error;
|
|
355
|
+
return error.failure === 'not-found' ? '' : undefined;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/** A run that wrote nothing, and why, said as `Codex: …`. */
|
|
360
|
+
function refused(why) {
|
|
361
|
+
return { outcome: 'refused', output: `Codex: ${why}\n` };
|
|
159
362
|
}
|
|
160
|
-
/**
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
363
|
+
/** AO14: what a trial's failure is said as, or `undefined` where the command ran and exited 0. */
|
|
364
|
+
function trialFailure(commandLine, outcome) {
|
|
365
|
+
if (outcome.kind === 'ran' && outcome.exitCode === 0)
|
|
366
|
+
return undefined;
|
|
367
|
+
if (outcome.kind === 'ran')
|
|
368
|
+
return `"${commandLine}" exited with code ${outcome.exitCode}${outcome.stderr === '' ? '' : ` (${outcome.stderr})`} when tried as Codex will run it,`;
|
|
369
|
+
if (outcome.kind === 'timed-out')
|
|
370
|
+
return `"${commandLine}" did not finish within ${USER_HOOKS.timeoutSeconds} seconds when tried as Codex will run it,`;
|
|
371
|
+
if (outcome.kind === 'not-started')
|
|
372
|
+
return `"${commandLine}" could not be started (${outcome.reason}),`;
|
|
373
|
+
return 'agentwhy cannot try the check on this system the way Codex runs it, which was measured on macOS and Linux only,';
|
|
165
374
|
}
|
|
166
|
-
/**
|
|
167
|
-
function
|
|
168
|
-
return
|
|
169
|
-
? "Codex: nothing to write. agentwhy blocks Codex with the same hook it blocks Claude Code with, and refuse doesn't run here yet: agentwhy init --refuse --codex\n"
|
|
170
|
-
: `Codex: ${WHERE} already runs agentwhy's hook. ${UNTIL_APPROVED}\n`;
|
|
375
|
+
/** `--codex`, with or without `--remove`, and nothing else: no hook named, nothing to protect or take out, no update. */
|
|
376
|
+
function nothingElse(options) {
|
|
377
|
+
return options.hooks === undefined && options.protect.length === 0 && (options.unprotect ?? []).length === 0 && options.update !== true;
|
|
171
378
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { join } from 'node:path';
|
|
2
4
|
import { SETTINGS_FILES } from '../adapter/claude-code/contract/settings.js';
|
|
3
5
|
import { denyEntriesFor, fileRulesIn, isDenied, withDenyEntries, withoutDenyEntries } from '../adapter/claude-code/settings/deny-entries.js';
|
package/dist/shared/compare.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* A short name for a content, so that two contents can be told apart without keeping either (`live-pages` L1). FNV-1a,
|
|
3
5
|
* 32 bits, in base 36: it guards nothing - a page only asks "is my file still what I was sent?" - and a collision costs
|
package/dist/shared/counter.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/** A moment as part of a file name: it sorts by time, and holds no character a file system refuses. */
|
|
2
4
|
export function fileStamp(now) {
|
|
3
5
|
return new Date(now).toISOString().replace(/[:.]/g, '-');
|
package/dist/shared/label.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
// Labels come only from structural positions: keys, line types, tool names, enumerations. A label must look
|
|
2
4
|
// like an identifier, and anything else is replaced, so free text cannot reach the output even from a
|
|
3
5
|
// malformed transcript. Known limit: an identifier-shaped secret placed in a structural position would pass;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* The name agentwhy is published under. npm refuses the unscoped `agentwhy` as too close to `agent-why`, so the package
|
|
3
5
|
* is scoped while the command it installs stays `agentwhy`. Written into a hook by the finder and read back from one by
|
package/dist/shared/printable.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* Text from a file as a terminal may be shown it: every character a terminal obeys rather than draws, as a space. A
|
|
3
5
|
* settings file, a transcript and a folder's name all arrive from outside this tool - a committed settings file and a
|
package/dist/shared/sentence.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* The first sentence of a piece of writing, capped (`specs/2026-09-15-why-this-call.md` §5.1, level B). Measured on one
|
|
3
5
|
* session: 311 of 315 first sentences run under 200 characters, so the cap is rarely reached - and when
|
package/dist/shared/wrap.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* Breaks text into lines that fit a width, at spaces where it can and inside a long run where it must - a URL
|
|
3
5
|
* or a path is never silently truncated, because a report that hides half a finding is worse than a wide line.
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentwhy/cli",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Reconstructs from transcripts why an AI agent reached for protected data",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
|
+
"author": "Nessprim Karol Kozer",
|
|
7
8
|
"repository": {
|
|
8
9
|
"type": "git",
|
|
9
10
|
"url": "git+https://github.com/karolkozer/agentwhy.git"
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A continuation request, so the refusal is said in the conversation by Codex, in the user's language (CK13). Codex's
|
|
3
|
-
* VS Code chat folds the hook's own words into the turn's collapsed steps, so the reply is the one place a person sees
|
|
4
|
-
* agentwhy there: it is asked to lead with agentwhy's name. The person reads this request too - the VS Code chat shows
|
|
5
|
-
* it as "Hook feedback" and the terminal as "Blocked by hook" (CKB14) - so it is three short sentences in the page's
|
|
6
|
-
* words (guidelines §7): what happened, what to say, and what not to do. It gives no text to copy.
|
|
7
|
-
*/
|
|
8
|
-
export function codexStopInstruction(refusals) {
|
|
9
|
-
const paths = [...new Set(refusals.map((refusal) => refusal.path))];
|
|
10
|
-
const named = paths.slice(0, 3).map((path) => JSON.stringify(path));
|
|
11
|
-
if (paths.length > 3)
|
|
12
|
-
named.push(`${paths.length - 3} more`);
|
|
13
|
-
const listed = named.length === 1 ? named[0] : `${named.slice(0, -1).join(', ')} and ${named.at(-1)}`;
|
|
14
|
-
const one = refusals.length === 1;
|
|
15
|
-
const [files, them] = paths.length === 1 ? ['file', 'it'] : ['files', 'them'];
|
|
16
|
-
return [
|
|
17
|
-
`agentwhy stopped ${one ? 'a command' : 'commands'} that would have read the private ${files} ${listed}, so ${one ? 'it' : 'they'} did not run.`,
|
|
18
|
-
`Say so to the user in one short message in their language, starting with **agentwhy**, and add that they can open the ${files} in their editor.`,
|
|
19
|
-
`Don't read ${them} another way or ask for what's in ${them}.`,
|
|
20
|
-
].join(' ');
|
|
21
|
-
}
|