@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
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import {} from '../../../shared/json.js';
|
|
5
|
+
import { HOOK_HASH, USER_HOOKS } from '../contract/hooks.js';
|
|
6
|
+
/**
|
|
7
|
+
* The entry's hash as Codex computes it (`2026-10-02-codex-approves-its-own-hook.md` AOB1): compact JSON, keys sorted at
|
|
8
|
+
* every depth, SHA-256. `undefined` where the entry holds something the recipe was not measured over - never a guess
|
|
9
|
+
* that Codex would agree (AO2).
|
|
10
|
+
*/
|
|
11
|
+
export function entryHash(placed) {
|
|
12
|
+
const { entry } = placed;
|
|
13
|
+
if (entry.type !== 'command' || typeof entry.command !== 'string')
|
|
14
|
+
return undefined;
|
|
15
|
+
if (HOOK_HASH.unmeasured.some((field) => field in entry))
|
|
16
|
+
return undefined;
|
|
17
|
+
const timeout = entry[HOOK_HASH.fields.timeout];
|
|
18
|
+
const asynchronous = entry[HOOK_HASH.fields.async];
|
|
19
|
+
const message = entry[HOOK_HASH.fields.statusMessage];
|
|
20
|
+
if (timeout !== undefined && (typeof timeout !== 'number' || !Number.isInteger(timeout)))
|
|
21
|
+
return undefined;
|
|
22
|
+
if (asynchronous !== undefined && typeof asynchronous !== 'boolean')
|
|
23
|
+
return undefined;
|
|
24
|
+
if (message !== undefined && typeof message !== 'string')
|
|
25
|
+
return undefined;
|
|
26
|
+
const handler = {
|
|
27
|
+
type: 'command',
|
|
28
|
+
command: entry.command,
|
|
29
|
+
[HOOK_HASH.fields.timeout]: Math.max(1, typeof timeout === 'number' ? timeout : HOOK_HASH.defaultTimeout),
|
|
30
|
+
[HOOK_HASH.fields.async]: asynchronous === true,
|
|
31
|
+
...(message === undefined ? {} : { [HOOK_HASH.fields.statusMessage]: message }),
|
|
32
|
+
};
|
|
33
|
+
const identity = { event_name: HOOK_HASH.eventKeys[placed.event], hooks: [handler] };
|
|
34
|
+
// A Stop group's matcher is dropped before hashing; PreToolUse keeps its own (AOB1, `matcher_pattern_for_event`).
|
|
35
|
+
if (placed.event === 'PreToolUse' && placed.matcher !== undefined)
|
|
36
|
+
identity.matcher = placed.matcher;
|
|
37
|
+
return `${HOOK_HASH.prefix}${createHash('sha256').update(JSON.stringify(sorted(identity))).digest('hex')}`;
|
|
38
|
+
}
|
|
39
|
+
/** The key Codex files an approval under: the hooks file's absolute path, the event in snake case, the two indices. */
|
|
40
|
+
export function approvalKey(hooksPath, placed) {
|
|
41
|
+
return `${hooksPath}:${HOOK_HASH.eventKeys[placed.event]}:${placed.group}:${placed.handler}`;
|
|
42
|
+
}
|
|
43
|
+
/** The approval filed under `key` in the one form this reads, or `undefined` where there is none. */
|
|
44
|
+
export function approvalOf(toml, key) {
|
|
45
|
+
const table = scan(toml).tables.find((candidate) => candidate.key === key);
|
|
46
|
+
return table === undefined ? undefined : { ...(table.hash === undefined ? {} : { hash: table.hash }), disabled: table.disabled };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The config with agentwhy's own approvals in place (AO2, AO3): each wanted key's table holds the wanted hash - its
|
|
50
|
+
* `trusted_hash` line replaced, or added to the table, or a table appended at the end - and a stale table is removed:
|
|
51
|
+
* one under the same hooks file, at a key no longer wanted, holding one of the wanted hashes, which is agentwhy's own
|
|
52
|
+
* approval left behind when its entries' positions shifted. A person's `enabled = false` on such a table moves with it
|
|
53
|
+
* to the new key of the same event. Every other line stays as it was. `unsafe` where agentwhy's key, or `hooks`
|
|
54
|
+
* itself, is written in a form a line edit cannot extend without making the file invalid (AO11) - its own table
|
|
55
|
+
* holding a `trusted_hash`, or an `enabled` the carried choice would replace, in a form this does not read, included:
|
|
56
|
+
* a second line of the same key is invalid TOML, and Codex would not start on it.
|
|
57
|
+
*/
|
|
58
|
+
export function withOwnApprovals(toml, hooksPath, wanted) {
|
|
59
|
+
const keys = new Set(wanted.map((approval) => approval.key));
|
|
60
|
+
const hashes = new Set(wanted.map((approval) => approval.hash));
|
|
61
|
+
const scanned = scan(toml);
|
|
62
|
+
const stale = scanned.tables.filter((table) => table.key.startsWith(`${hooksPath}:`) && !keys.has(table.key) && table.hash !== undefined && hashes.has(table.hash));
|
|
63
|
+
const unsafe = unsafeFor(scanned, [...keys, ...stale.map((table) => table.key)]);
|
|
64
|
+
if (unsafe !== undefined)
|
|
65
|
+
return { kind: 'unsafe', reason: unsafe };
|
|
66
|
+
// A disabled stale table hands its choice to the wanted key of the same event.
|
|
67
|
+
const carried = new Set();
|
|
68
|
+
for (const table of stale) {
|
|
69
|
+
if (!table.disabled)
|
|
70
|
+
continue;
|
|
71
|
+
const event = eventOf(table.key, hooksPath);
|
|
72
|
+
const target = wanted.find((approval) => eventOf(approval.key, hooksPath) === event)?.key;
|
|
73
|
+
if (target !== undefined)
|
|
74
|
+
carried.add(target);
|
|
75
|
+
}
|
|
76
|
+
// A key written in a form the line regexes do not read is not absent: adding the line beside it duplicates the key.
|
|
77
|
+
for (const approval of wanted) {
|
|
78
|
+
const table = scanned.tables.find((candidate) => candidate.key === approval.key);
|
|
79
|
+
if (table === undefined)
|
|
80
|
+
continue;
|
|
81
|
+
if (table.otherHash)
|
|
82
|
+
return { kind: 'unsafe', reason: WRITTEN_OTHERWISE };
|
|
83
|
+
if (carried.has(approval.key) && !table.disabled && table.enabled)
|
|
84
|
+
return { kind: 'unsafe', reason: WRITTEN_OTHERWISE };
|
|
85
|
+
}
|
|
86
|
+
const lines = [...scanned.lines];
|
|
87
|
+
const removed = new Set();
|
|
88
|
+
for (const table of stale)
|
|
89
|
+
for (const at of rangeOf(scanned, table))
|
|
90
|
+
removed.add(at);
|
|
91
|
+
const inserted = new Map();
|
|
92
|
+
const appended = [];
|
|
93
|
+
for (const approval of wanted) {
|
|
94
|
+
const table = scanned.tables.find((candidate) => candidate.key === approval.key);
|
|
95
|
+
const hashLine = `${USER_HOOKS.trustedHash} = "${approval.hash}"`;
|
|
96
|
+
if (table === undefined) {
|
|
97
|
+
appended.push([headerFor(approval.key), OWN_COMMENT, ...(carried.has(approval.key) ? [`${USER_HOOKS.enabled} = false`] : []), hashLine].join('\n'));
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (table.hashLine !== undefined)
|
|
101
|
+
lines[table.hashLine] = hashLine;
|
|
102
|
+
else
|
|
103
|
+
inserted.set(table.start, [...(inserted.get(table.start) ?? []), hashLine]);
|
|
104
|
+
if (carried.has(approval.key) && !table.disabled)
|
|
105
|
+
inserted.set(table.start, [...(inserted.get(table.start) ?? []), `${USER_HOOKS.enabled} = false`]);
|
|
106
|
+
}
|
|
107
|
+
const out = [];
|
|
108
|
+
lines.forEach((line, at) => {
|
|
109
|
+
if (removed.has(at))
|
|
110
|
+
return;
|
|
111
|
+
out.push(line);
|
|
112
|
+
for (const extra of inserted.get(at) ?? [])
|
|
113
|
+
out.push(extra);
|
|
114
|
+
});
|
|
115
|
+
let text = out.join('\n');
|
|
116
|
+
if (appended.length > 0) {
|
|
117
|
+
const base = text === '' ? '' : text.endsWith('\n') ? text : `${text}\n`;
|
|
118
|
+
text = `${base}${base === '' ? '' : '\n'}${appended.join('\n\n')}\n`;
|
|
119
|
+
}
|
|
120
|
+
return { kind: 'edited', text };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The config without agentwhy's approvals (AO7): under the given hooks file, every table at one of `keys` or holding
|
|
124
|
+
* one of `hashes` goes whole - an `enabled = false` in it included, since it named agentwhy's entry and would otherwise
|
|
125
|
+
* apply to whatever lands at that position next. Every other line stays as it was.
|
|
126
|
+
*/
|
|
127
|
+
export function withoutOwnApprovals(toml, hooksPath, keys, hashes) {
|
|
128
|
+
const scanned = scan(toml);
|
|
129
|
+
const ours = scanned.tables.filter((table) => table.key.startsWith(`${hooksPath}:`) && (keys.has(table.key) || (table.hash !== undefined && hashes.has(table.hash))));
|
|
130
|
+
const unsafe = unsafeFor(scanned, [...keys, ...ours.map((table) => table.key)]);
|
|
131
|
+
if (unsafe !== undefined)
|
|
132
|
+
return { kind: 'unsafe', reason: unsafe };
|
|
133
|
+
const removed = new Set();
|
|
134
|
+
for (const table of ours)
|
|
135
|
+
for (const at of rangeOf(scanned, table))
|
|
136
|
+
removed.add(at);
|
|
137
|
+
return { kind: 'edited', text: scanned.lines.filter((_, at) => !removed.has(at)).join('\n') };
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The lines a table's removal takes: its header through its last non-blank line - the blank lines after it stay, as
|
|
141
|
+
* the separator before whatever follows, or the file's last newline - and the blank line before it where agentwhy wrote
|
|
142
|
+
* that one with it.
|
|
143
|
+
*/
|
|
144
|
+
function rangeOf(scanned, table) {
|
|
145
|
+
let last = table.end - 1;
|
|
146
|
+
while (last > table.start && (scanned.lines[last] ?? '').trim() === '')
|
|
147
|
+
last -= 1;
|
|
148
|
+
const range = [];
|
|
149
|
+
for (let at = table.start; at <= last; at += 1)
|
|
150
|
+
range.push(at);
|
|
151
|
+
if (table.start > 0 && scanned.lines[table.start - 1] === '' && scanned.lines[table.start + 1] === OWN_COMMENT)
|
|
152
|
+
range.push(table.start - 1);
|
|
153
|
+
return range;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* AO16: whether `config.toml` defines hooks of its own - a `[hooks.<Event>]` or `[[hooks.<Event>]]` table, a key of
|
|
157
|
+
* `[hooks]` other than `state`, or `hooks` written inline or dotted at the root with anything but `state`. Codex would
|
|
158
|
+
* then load hooks from both it and a `hooks.json` agentwhy created, and warn about it in every session.
|
|
159
|
+
*/
|
|
160
|
+
export function tomlDefinesHooks(toml) {
|
|
161
|
+
const scanned = scan(toml);
|
|
162
|
+
let context = 'root';
|
|
163
|
+
for (let at = 0; at < scanned.lines.length; at += 1) {
|
|
164
|
+
if (scanned.opaque[at])
|
|
165
|
+
continue;
|
|
166
|
+
const line = scanned.lines[at] ?? '';
|
|
167
|
+
if (HEADER.test(line)) {
|
|
168
|
+
const event = HOOKS_SUBTABLE.exec(line)?.[2];
|
|
169
|
+
if (event !== undefined && event !== 'state')
|
|
170
|
+
return true;
|
|
171
|
+
context = HOOKS_TABLE.test(line) ? 'hooks' : 'other';
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (context === 'root' && /^\s*hooks\s*=/.test(line))
|
|
175
|
+
return true;
|
|
176
|
+
const dotted = /^\s*hooks\s*\.\s*("?)(\w+)\1/.exec(line)?.[2];
|
|
177
|
+
if (context === 'root' && dotted !== undefined && dotted !== 'state')
|
|
178
|
+
return true;
|
|
179
|
+
const key = /^\s*("?)(\w+)\1\s*[=.]/.exec(line)?.[2];
|
|
180
|
+
if (context === 'hooks' && key !== undefined && key !== 'state')
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
const HOOKS_SUBTABLE = /^\s*\[\[?\s*hooks\s*\.\s*("?)(\w+)\1/;
|
|
186
|
+
/** Why a line edit is refused where agentwhy's own approval is written in a form this does not read (AO11). */
|
|
187
|
+
const WRITTEN_OTHERWISE = 'agentwhy\'s approval is written there in a form agentwhy does not edit';
|
|
188
|
+
/** The comment agentwhy writes under its own header, so a person reading the file knows what it is and how to undo it. */
|
|
189
|
+
const OWN_COMMENT = '# agentwhy approved its own check here; `agentwhy init --remove --codex` takes it out.';
|
|
190
|
+
const HEADER = /^\s*\[/;
|
|
191
|
+
const STATE_HEADER = /^\s*\[\s*hooks\s*\.\s*state\s*\.\s*"((?:[^"\\]|\\.)*)"\s*\]\s*(?:#.*)?$/;
|
|
192
|
+
const HOOKS_TABLE = /^\s*\[\s*hooks\s*\]\s*(?:#.*)?$/;
|
|
193
|
+
const STATE_TABLE = /^\s*\[\s*hooks\s*\.\s*state\s*\]\s*(?:#.*)?$/;
|
|
194
|
+
const TRUSTED_HASH = new RegExp(`^\\s*${USER_HOOKS.trustedHash}\\s*=\\s*"([^"\\\\]*)"\\s*(?:#.*)?$`);
|
|
195
|
+
const DISABLED = new RegExp(`^\\s*${USER_HOOKS.enabled}\\s*=\\s*false\\s*(?:#.*)?$`);
|
|
196
|
+
/** The two keys as a line names one, whatever its value is written like: what a line edit would duplicate. */
|
|
197
|
+
const HASH_KEY = new RegExp(`^\\s*("?)${USER_HOOKS.trustedHash}\\1\\s*=`);
|
|
198
|
+
const ENABLED_KEY = new RegExp(`^\\s*("?)${USER_HOOKS.enabled}\\1\\s*=`);
|
|
199
|
+
/** The file as lines, which of them are inside multi-line strings, and every `hooks.state` table in the form written. */
|
|
200
|
+
function scan(toml) {
|
|
201
|
+
const lines = toml.split('\n');
|
|
202
|
+
const opaque = [];
|
|
203
|
+
let open;
|
|
204
|
+
for (const line of lines) {
|
|
205
|
+
opaque.push(open !== undefined);
|
|
206
|
+
open = afterLine(line, open);
|
|
207
|
+
}
|
|
208
|
+
const tables = [];
|
|
209
|
+
for (let at = 0; at < lines.length; at += 1) {
|
|
210
|
+
if (opaque[at])
|
|
211
|
+
continue;
|
|
212
|
+
const raw = STATE_HEADER.exec(lines[at] ?? '')?.[1];
|
|
213
|
+
if (raw === undefined)
|
|
214
|
+
continue;
|
|
215
|
+
let end = at + 1;
|
|
216
|
+
while (end < lines.length && (opaque[end] || !HEADER.test(lines[end] ?? '')))
|
|
217
|
+
end += 1;
|
|
218
|
+
let hash;
|
|
219
|
+
let hashLine;
|
|
220
|
+
let disabled = false;
|
|
221
|
+
let otherHash = false;
|
|
222
|
+
let enabled = false;
|
|
223
|
+
for (let inner = at + 1; inner < end; inner += 1) {
|
|
224
|
+
if (opaque[inner])
|
|
225
|
+
continue;
|
|
226
|
+
const line = lines[inner] ?? '';
|
|
227
|
+
const found = TRUSTED_HASH.exec(line)?.[1];
|
|
228
|
+
if (found !== undefined) {
|
|
229
|
+
hash = found;
|
|
230
|
+
hashLine = inner;
|
|
231
|
+
}
|
|
232
|
+
else if (HASH_KEY.test(line))
|
|
233
|
+
otherHash = true;
|
|
234
|
+
if (DISABLED.test(line))
|
|
235
|
+
disabled = true;
|
|
236
|
+
if (ENABLED_KEY.test(line))
|
|
237
|
+
enabled = true;
|
|
238
|
+
}
|
|
239
|
+
tables.push({
|
|
240
|
+
key: unescapeBasic(raw),
|
|
241
|
+
start: at,
|
|
242
|
+
end,
|
|
243
|
+
...(hash === undefined ? {} : { hash }),
|
|
244
|
+
...(hashLine === undefined ? {} : { hashLine }),
|
|
245
|
+
disabled,
|
|
246
|
+
otherHash,
|
|
247
|
+
enabled,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return { lines, opaque, tables };
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* The multi-line string still open at the end of `line`, given the one open at its start. The line is read the way
|
|
254
|
+
* TOML reads it, so a delimiter that is not one holds nothing open: `"""` after a `#` is a comment, `'''` inside a
|
|
255
|
+
* quoted Windows path is part of the path, and a backslash escapes the character after it in a basic string. Counting
|
|
256
|
+
* delimiters per line instead would let one odd occurrence mark the rest of the file opaque, hiding an existing
|
|
257
|
+
* approval and appending a second table under the same key (AO11).
|
|
258
|
+
*/
|
|
259
|
+
function afterLine(line, open) {
|
|
260
|
+
let inside = open;
|
|
261
|
+
let at = 0;
|
|
262
|
+
while (at < line.length) {
|
|
263
|
+
if (inside !== undefined) {
|
|
264
|
+
if (inside === '"""' && line[at] === '\\') {
|
|
265
|
+
at += 2;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (line.startsWith(inside, at)) {
|
|
269
|
+
inside = undefined;
|
|
270
|
+
at += 3;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
at += 1;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const char = line[at];
|
|
277
|
+
// Outside a string a `#` starts a comment, and nothing after it on the line is structure.
|
|
278
|
+
if (char === '#')
|
|
279
|
+
return undefined;
|
|
280
|
+
if (char === '"' || char === "'") {
|
|
281
|
+
const triple = char.repeat(3);
|
|
282
|
+
if (line.startsWith(triple, at)) {
|
|
283
|
+
inside = triple;
|
|
284
|
+
at += 3;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
at = afterSingle(line, at + 1, char);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
at += 1;
|
|
291
|
+
}
|
|
292
|
+
return inside;
|
|
293
|
+
}
|
|
294
|
+
/** The index just past a single-line string opened at `at` with `quote`, or the line's end where it is unterminated. */
|
|
295
|
+
function afterSingle(line, at, quote) {
|
|
296
|
+
let index = at;
|
|
297
|
+
while (index < line.length) {
|
|
298
|
+
if (quote === '"' && line[index] === '\\') {
|
|
299
|
+
index += 2;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (line[index] === quote)
|
|
303
|
+
return index + 1;
|
|
304
|
+
index += 1;
|
|
305
|
+
}
|
|
306
|
+
return line.length;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* AO11: why a line edit of these keys would make the file invalid, or `undefined` where it would not. `hooks` written
|
|
310
|
+
* inline or as a dotted key at the root, `state` defined inside `[hooks]`, one of the keys as a key of `[hooks.state]`
|
|
311
|
+
* or in any header but the plain `[hooks.state."<key>"]`, or that header twice.
|
|
312
|
+
*/
|
|
313
|
+
function unsafeFor(scanned, keys) {
|
|
314
|
+
const quoted = keys.flatMap((key) => [`"${escapeBasic(key)}"`, `'${key}'`]);
|
|
315
|
+
let context = 'root';
|
|
316
|
+
const seen = new Map();
|
|
317
|
+
for (let at = 0; at < scanned.lines.length; at += 1) {
|
|
318
|
+
if (scanned.opaque[at])
|
|
319
|
+
continue;
|
|
320
|
+
const line = scanned.lines[at] ?? '';
|
|
321
|
+
if (HEADER.test(line)) {
|
|
322
|
+
const state = STATE_HEADER.exec(line)?.[1];
|
|
323
|
+
if (state !== undefined) {
|
|
324
|
+
const key = unescapeBasic(state);
|
|
325
|
+
seen.set(key, (seen.get(key) ?? 0) + 1);
|
|
326
|
+
if ((seen.get(key) ?? 0) > 1 && keys.includes(key))
|
|
327
|
+
return 'agentwhy\'s approval appears twice there';
|
|
328
|
+
context = 'other';
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (quoted.some((text) => line.includes(text)))
|
|
332
|
+
return WRITTEN_OTHERWISE;
|
|
333
|
+
context = HOOKS_TABLE.test(line) ? 'hooks' : STATE_TABLE.test(line) ? 'state' : 'other';
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (context === 'root' && /^\s*hooks\s*[=.]/.test(line))
|
|
337
|
+
return 'its hooks are written inline there';
|
|
338
|
+
if (context === 'hooks' && /^\s*state\s*[=.]/.test(line))
|
|
339
|
+
return 'its hooks are written inline there';
|
|
340
|
+
if (context === 'state' && quoted.some((text) => line.trimStart().startsWith(text)))
|
|
341
|
+
return WRITTEN_OTHERWISE;
|
|
342
|
+
}
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
function headerFor(key) {
|
|
346
|
+
return `[hooks.state."${escapeBasic(key)}"]`;
|
|
347
|
+
}
|
|
348
|
+
/** The event part of a key under `hooksPath`: `pre_tool_use` or `stop`. */
|
|
349
|
+
function eventOf(key, hooksPath) {
|
|
350
|
+
return key.slice(hooksPath.length + 1).split(':')[0];
|
|
351
|
+
}
|
|
352
|
+
/** A TOML basic string's escapes for a path: backslashes and quotes. Other characters are written as they are. */
|
|
353
|
+
function escapeBasic(key) {
|
|
354
|
+
return key.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
|
|
355
|
+
}
|
|
356
|
+
function unescapeBasic(raw) {
|
|
357
|
+
return raw.replaceAll(/\\(u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|.)/g, (_, escape) => {
|
|
358
|
+
if (escape.length > 1)
|
|
359
|
+
return String.fromCodePoint(Number.parseInt(escape.slice(1), 16));
|
|
360
|
+
return { b: '\b', t: '\t', n: '\n', f: '\f', r: '\r' }[escape] ?? escape;
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
/** Object keys ordered at every depth, as the hash is taken over (AOB1). */
|
|
364
|
+
function sorted(value) {
|
|
365
|
+
if (Array.isArray(value))
|
|
366
|
+
return value.map(sorted);
|
|
367
|
+
if (typeof value === 'object' && value !== null) {
|
|
368
|
+
const record = value;
|
|
369
|
+
return Object.fromEntries(Object.keys(record)
|
|
370
|
+
.sort()
|
|
371
|
+
.map((key) => [key, sorted(record[key])]));
|
|
372
|
+
}
|
|
373
|
+
return value;
|
|
374
|
+
}
|
|
@@ -3,12 +3,16 @@ const HELP_FLAGS = ['--help', '-h'];
|
|
|
3
3
|
export class CommandRouter {
|
|
4
4
|
#commands;
|
|
5
5
|
#withoutACommand;
|
|
6
|
+
#missingCommand;
|
|
6
7
|
/**
|
|
7
8
|
* `withoutACommand` answers `agentwhy` with nothing after it, and with arguments that begin with `-` (they are its
|
|
8
9
|
* flags); without one, that stays a usage error. It may also be registered under its own name.
|
|
10
|
+
*
|
|
11
|
+
* `missingCommand` is said under that usage error alone, for whoever ran a bare `agentwhy` where nothing answers it.
|
|
9
12
|
*/
|
|
10
|
-
constructor(commands, withoutACommand) {
|
|
13
|
+
constructor(commands, withoutACommand, missingCommand) {
|
|
11
14
|
this.#withoutACommand = withoutACommand;
|
|
15
|
+
this.#missingCommand = missingCommand;
|
|
12
16
|
const byName = new Map();
|
|
13
17
|
for (const command of commands) {
|
|
14
18
|
if (byName.has(command.name))
|
|
@@ -27,7 +31,9 @@ export class CommandRouter {
|
|
|
27
31
|
if (name !== undefined && HELP_FLAGS.includes(name))
|
|
28
32
|
return { kind: 'help', usage: this.usage };
|
|
29
33
|
if (name === undefined) {
|
|
30
|
-
|
|
34
|
+
if (this.#withoutACommand !== undefined)
|
|
35
|
+
return this.#withoutACommand.execute([]);
|
|
36
|
+
return { ...this.#usageError('missing command'), ...(this.#missingCommand === undefined ? {} : { hint: this.#missingCommand }) };
|
|
31
37
|
}
|
|
32
38
|
if (name.startsWith('-') && this.#withoutACommand !== undefined)
|
|
33
39
|
return this.#withoutACommand.execute(argv);
|
|
@@ -1,27 +1,30 @@
|
|
|
1
|
-
import { codexStopInstruction } from '../../refuse/render/codex-stop-words.js';
|
|
2
1
|
import { EXIT_CODE } from '../exit-codes.js';
|
|
3
2
|
const USAGE = `Usage: agentwhy codex-stop --codex
|
|
4
3
|
|
|
5
|
-
Run by the Codex Stop hook.
|
|
6
|
-
|
|
4
|
+
Run by the Codex Stop hook. At the end of a Codex turn it says what agentwhy says at the end of a
|
|
5
|
+
Claude Code one: a command it stopped, a key from a private file now in the chat, a private file
|
|
6
|
+
you let the AI read, or that nothing private was opened. It reads the turn's local transcript and
|
|
7
|
+
never sends it anywhere.
|
|
7
8
|
`;
|
|
9
|
+
/**
|
|
10
|
+
* Codex's Stop hook (`2026-10-02-codex-says-it-too.md` CX1). Its command is the one setup has always written and a
|
|
11
|
+
* person has approved in Codex (CK13), so what it says grew without asking anyone to approve it again: it runs `watch`
|
|
12
|
+
* for Codex, which says first a command agentwhy stopped (CX2), then what `watch` says at Claude Code's `Stop`.
|
|
13
|
+
*/
|
|
8
14
|
export class CodexStopCliCommand {
|
|
9
15
|
name = 'codex-stop';
|
|
10
16
|
usage = USAGE;
|
|
11
|
-
#
|
|
17
|
+
#watch;
|
|
12
18
|
#interactive;
|
|
13
|
-
constructor(
|
|
14
|
-
this.#
|
|
19
|
+
constructor(watch, interactive) {
|
|
20
|
+
this.#watch = watch;
|
|
15
21
|
this.#interactive = interactive;
|
|
16
22
|
}
|
|
17
23
|
async execute(args) {
|
|
18
24
|
if (args.length !== 1 || args[0] !== '--codex' || this.#interactive)
|
|
19
25
|
return { kind: 'usage-error', message: 'codex-stop reads Codex hook input on standard input', usage: this.usage };
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
output: refusals.length === 0 ? '' : JSON.stringify({ decision: 'block', reason: codexStopInstruction(refusals) }),
|
|
24
|
-
exitCode: EXIT_CODE.ok,
|
|
25
|
-
};
|
|
26
|
+
// A hook never exits 2 and never fails loudly: whatever `watch` found to say is all it prints (R8).
|
|
27
|
+
const result = await this.#watch.run({});
|
|
28
|
+
return { kind: 'completed', output: result.output, exitCode: EXIT_CODE.ok };
|
|
26
29
|
}
|
|
27
30
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
export const DOCTOR_USAGE = `Usage: agentwhy doctor --input <session-dir | session.jsonl | rollout.jsonl | rollout-folder> [--json]
|
|
2
4
|
|
|
3
5
|
Reports the structure of one Claude Code session - its sources, line types, tools,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { parseArgs } from 'node:util';
|
|
2
4
|
import { EXIT_CODE } from '../exit-codes.js';
|
|
3
5
|
export const INIT_USAGE = `Usage: agentwhy init [--watch] [--refuse] [--protect <pattern>] [--shared] [--codex]
|
|
@@ -7,9 +9,10 @@ export const INIT_USAGE = `Usage: agentwhy init [--watch] [--refuse] [--protect
|
|
|
7
9
|
Sets up the current project, and undoes it. At a terminal it asks who it is for, what agentwhy
|
|
8
10
|
should do here and what else to protect, then shows what it will write and asks. It writes to
|
|
9
11
|
.claude/settings.local.json, the settings file that is yours alone and stays out of the repository,
|
|
10
|
-
unless you say the change is for everyone.
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
unless you say the change is for everyone. On a computer that uses Codex (~/.codex exists), it also
|
|
13
|
+
writes refuse into your own ~/.codex/hooks.json and approves those entries - its own and nothing
|
|
14
|
+
else, tried once first the way Codex runs them - so a file blocked here is blocked in Codex from
|
|
15
|
+
the first message, in the terminal and in VS Code, with nothing to approve in Codex.
|
|
13
16
|
|
|
14
17
|
--watch agentwhy watch, when a delegated agent finishes: a notification when it
|
|
15
18
|
wrote a value from a protected file. Ticked in the list, and the one
|
|
@@ -22,8 +25,10 @@ once you approve agentwhy's hook in Codex, which runs a new hook only after that
|
|
|
22
25
|
for several. At a terminal you are asked for these as well
|
|
23
26
|
--shared write to .claude/settings.json, which is committed, so the hooks run for
|
|
24
27
|
everyone who clones. Default: .claude/settings.local.json, which is yours
|
|
25
|
-
--codex write Codex's
|
|
26
|
-
|
|
28
|
+
--codex alone: write and approve Codex's check, to match the refuse Claude Code
|
|
29
|
+
already runs here. With --remove: take agentwhy's entries and their
|
|
30
|
+
approvals out of ~/.codex too, which a plain --remove leaves for the
|
|
31
|
+
other projects that block files with them
|
|
27
32
|
--remove undo it. At a terminal it asks what to take out - each hook, and each deny
|
|
28
33
|
rule by the path it protects - and removes only what is ticked. With
|
|
29
34
|
--watch or --refuse, only that hook; deny rules then stay
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { parseArgs } from 'node:util';
|
|
2
4
|
import { ALERT_THRESHOLDS, DEFAULT_THRESHOLD } from '../../report/watch/agent-alert.js';
|
|
3
5
|
import { CLEAN_MODES, DEFAULT_CHANNELS, DEFAULT_CLEAN, DEFAULT_SAID_AS, NOTICE_CHANNELS, NOTICE_LANGS, SAID_AS, } from '../../report/watch/notice-choices.js';
|
|
@@ -132,7 +134,7 @@ function said(view) {
|
|
|
132
134
|
const LEVEL = {
|
|
133
135
|
value: ' - a value from a protected file is in the conversation',
|
|
134
136
|
reached: ' - that, and a protected file reached',
|
|
135
|
-
refused: ' - that, and an attempt
|
|
137
|
+
refused: ' - that, and an attempt that was stopped',
|
|
136
138
|
};
|
|
137
139
|
const QUIET = {
|
|
138
140
|
off: ' - nothing is said about a turn that found nothing',
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { parseArgs } from 'node:util';
|
|
2
4
|
import { notCheckedMessage, refusalReason } from '../../refuse/render/refusal-words.js';
|
|
3
5
|
import { EXIT_CODE } from '../exit-codes.js';
|
|
@@ -11,8 +13,10 @@ can ask the user. Anything else runs as it would have.
|
|
|
11
13
|
|
|
12
14
|
--policy <file> the policy to apply, as for \`report\`
|
|
13
15
|
--settings <file> a settings file whose deny rules stand in for a policy, as for \`report\`
|
|
14
|
-
--codex the input is Codex's
|
|
15
|
-
|
|
16
|
+
--codex the input is Codex's. The project is the nearest folder at or above the
|
|
17
|
+
command's whose Claude Code settings run refuse, short of the home folder.
|
|
18
|
+
Without --settings or --policy, the rules are the ones that project's refuse
|
|
19
|
+
names; where no such folder is found, the command runs with no message
|
|
16
20
|
-h, --help this text
|
|
17
21
|
|
|
18
22
|
It reads a command line, so it does NOT stop: a path held in a variable, reached after cd, or
|
|
@@ -22,8 +26,9 @@ those. When it cannot read its input or its policy, or a search reaches more fil
|
|
|
22
26
|
look through, it lets the command run and warns the user, so a typo in a settings path cannot
|
|
23
27
|
stop every command.
|
|
24
28
|
|
|
25
|
-
Exit 2 refuses the command; 0 lets it run. agentwhy init --refuse installs the hook, and
|
|
26
|
-
|
|
29
|
+
Exit 2 refuses the command; 0 lets it run. agentwhy init --refuse installs the hook, and on a
|
|
30
|
+
computer that uses Codex, Codex's too - written into your own ~/.codex files and approved there
|
|
31
|
+
by agentwhy itself, tried once first, so it runs from the first message.
|
|
27
32
|
`;
|
|
28
33
|
/** `agentwhy refuse` - the shell hook of `specs/2026-09-16-worth-running-every-day.md` R18-R22. */
|
|
29
34
|
export class RefuseCliCommand {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
export const DEFAULT_WIDTH = 100;
|
|
2
4
|
/** Narrower than this the layout stops being a layout; wider, a line is too long to run an eye back along. */
|
|
3
5
|
const WIDTH_BOUNDS = { least: 60, most: 120 };
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { parseArgs } from 'node:util';
|
|
2
4
|
import { parseSince, splitBySince } from '../../core/session-filter.js';
|
|
3
|
-
import { sessionKey } from '../../core/session-catalogue.js';
|
|
5
|
+
import { noStoreAnywhere, sessionKey } from '../../core/session-catalogue.js';
|
|
4
6
|
import { PROVIDER_NAMES } from '../../core/session-format.js';
|
|
5
7
|
import { recogniseAll } from '../../core/session-titles.js';
|
|
6
8
|
import { table } from '../../shared/text-table.js';
|
|
7
9
|
import { EXIT_CODE } from '../exit-codes.js';
|
|
8
10
|
import { EXIT_CODE_BY_OUTCOME } from './report-cli-command.js';
|
|
11
|
+
import { nothingSavedHere } from '../../report/nothing-saved-here.js';
|
|
9
12
|
export const SESSIONS_USAGE = `Usage: agentwhy sessions [--since <span | date>] [--no-interactive]
|
|
10
13
|
|
|
11
14
|
Lists the sessions Claude Code has kept for the current directory's project, newest first,
|
|
@@ -65,6 +68,11 @@ export class SessionsCliCommand {
|
|
|
65
68
|
const listing = await catalogue.list(workingDirectory);
|
|
66
69
|
// "Nothing here" and "looked in the wrong place" are different answers, and the second is far more likely
|
|
67
70
|
// to be the truth when a path encoding is involved.
|
|
71
|
+
if (!listing.found && noStoreAnywhere(listing)) {
|
|
72
|
+
const where = listing.searched.map((place) => place.directory).join(' and ');
|
|
73
|
+
const ways = 'use Claude Code or Codex here, then run agentwhy sessions again - or run agentwhy init now, to be protected in Claude Code before there is history to check.';
|
|
74
|
+
return { kind: 'completed', output: nothingSavedHere('Claude Code or Codex', where, ways), exitCode: EXIT_CODE.ok };
|
|
75
|
+
}
|
|
68
76
|
if (!listing.found) {
|
|
69
77
|
return {
|
|
70
78
|
kind: 'completed',
|