@agentwhy/cli 0.0.0-dev → 0.2.0
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/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 +10 -0
- package/dist/composition-root.js +80 -15
- 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 +75 -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.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 +15 -1
- 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 +69 -29
- package/dist/report/start/settings/alerts-tab.js +4 -1
- package/dist/report/start/settings/files-tab.js +18 -12
- package/dist/report/start/settings/general-tab.js +33 -1
- package/dist/report/start/settings/settings-renderer.js +2 -0
- package/dist/report/start/settings/settings-script.js +8 -1
- package/dist/report/start/settings/settings-view.js +29 -2
- package/dist/report/start/settings/settings-windows.js +16 -5
- package/dist/report/start/settings-files.js +2 -0
- package/dist/report/start/to-fix/done-list.js +2 -0
- package/dist/report/start/to-fix/file-window.js +2 -0
- package/dist/report/start/to-fix/to-fix-list.js +2 -0
- package/dist/report/start/to-fix/to-fix-renderer.js +2 -0
- package/dist/report/start/to-fix/to-fix-script.js +2 -0
- package/dist/report/watch/agent-alert.js +3 -0
- package/dist/report/watch/codex-turn-format.js +69 -0
- package/dist/report/watch/notice-choices.js +2 -0
- package/dist/report/watch/notice-settings.js +2 -0
- package/dist/report/watch/preferences.js +2 -0
- package/dist/report/watch/render/codex-request-words.js +106 -0
- package/dist/report/watch/render/instruction-words.js +114 -12
- package/dist/report/watch/render/notice-words.js +74 -4
- package/dist/report/watch/subagent-watch.js +270 -45
- package/dist/setup/behind.js +2 -0
- package/dist/setup/codex-mirror.js +311 -104
- package/dist/setup/not-a-project.js +2 -0
- package/dist/setup/project-setup.js +2 -0
- package/dist/shared/compare.js +2 -0
- package/dist/shared/content-version.js +2 -0
- package/dist/shared/counter.js +2 -0
- package/dist/shared/file-stamp.js +2 -0
- package/dist/shared/label.js +2 -0
- package/dist/shared/package-name.js +2 -0
- package/dist/shared/plain-invocation.js +2 -0
- package/dist/shared/printable.js +2 -0
- package/dist/shared/sentence.js +2 -0
- package/dist/shared/terminal-logo.js +2 -0
- package/dist/shared/text-table.js +2 -0
- package/dist/shared/wrap.js +2 -0
- package/package.json +2 -1
- package/dist/refuse/render/codex-stop-words.js +0 -21
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { createHash } from 'node:crypto';
|
|
2
4
|
import { displayPath, displayPathsIn, insideProject, forSharing, OUTSIDE_THE_PROJECT, } from '../project-root.js';
|
|
3
5
|
import { looksRandom } from './entropy.js';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* The exclusions of spec §5.4 — "the real work here". Without them class B is a noise generator, because a
|
|
3
5
|
* sensitive name sits beside a harmless value far more often than beside a secret: `SECRET_ENABLED=true`,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* Values read from protected resources, held as salted digests of their runs and asked about other text
|
|
3
5
|
* (`specs/2026-09-15-what-came-back.md` R3, R4). No value is kept and no run of one (§5.4 rule 6): what is kept is the digest
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether no AI's store of conversations was there at all, where this listing looked: then "not used here yet" is not
|
|
3
|
+
* the likely reason, and running somewhere conversations are not saved is (R28, amended 2026-10-01).
|
|
4
|
+
*/
|
|
5
|
+
export function noStoreAnywhere(listing) {
|
|
6
|
+
return listing.searched.length > 0 && listing.searched.every((place) => place.store === 'missing');
|
|
7
|
+
}
|
|
1
8
|
/**
|
|
2
9
|
* What names a session wherever two AIs' sessions meet - a row, a file, a cached report, `report --input`: its id where
|
|
3
10
|
* Claude Code wrote it, as it always was, and the id after its AI's name otherwise, so equal ids never overwrite each
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { isCancel, text } from '@clack/prompts';
|
|
2
4
|
/** One line of text, through `@clack/prompts`. A blank answer comes back as an empty string, never as a cancellation. */
|
|
3
5
|
export class ClackAsker {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { lstat, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
4
|
import { join } from 'node:path';
|
|
3
5
|
/** One directory of its own, so pruning never looks at a file this tool did not write. */
|
|
@@ -70,6 +72,9 @@ export class FileAlertStore {
|
|
|
70
72
|
}
|
|
71
73
|
return remembered;
|
|
72
74
|
}
|
|
75
|
+
peek(sessionId) {
|
|
76
|
+
return this.#read(sessionId);
|
|
77
|
+
}
|
|
73
78
|
/** A session id is not a file name until this makes it one: no separator, no `..`, no length worth worrying about. */
|
|
74
79
|
#fileFor(sessionId) {
|
|
75
80
|
const safe = sessionId.replace(UNSAFE, '-').slice(0, 80);
|
|
@@ -109,12 +114,14 @@ function isRemembered(value) {
|
|
|
109
114
|
return false;
|
|
110
115
|
const record = value;
|
|
111
116
|
return typeof record.agentId === 'string' && typeof record.level === 'string' && typeof record.words === 'string' &&
|
|
112
|
-
(record.counts === undefined || areCounts(record.counts))
|
|
117
|
+
(record.counts === undefined || areCounts(record.counts)) && (record.notByRule === undefined || record.notByRule === true) &&
|
|
118
|
+
(record.told === undefined || record.told === true) && (record.data === undefined || record.data === true);
|
|
113
119
|
}
|
|
114
|
-
/** Counts that are not
|
|
120
|
+
/** Counts that are not whole numbers are counts this did not write: the record is read as one without them. */
|
|
115
121
|
function areCounts(value) {
|
|
116
122
|
if (value === null || typeof value !== 'object')
|
|
117
123
|
return false;
|
|
118
124
|
const counts = value;
|
|
119
|
-
return Number.isInteger(counts.values) && Number.isInteger(counts.reached)
|
|
125
|
+
return Number.isInteger(counts.values) && Number.isInteger(counts.reached) &&
|
|
126
|
+
[counts.told, counts.data].every((count) => count === undefined || Number.isInteger(count));
|
|
120
127
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { appendFile, mkdir, readFile } from 'node:fs/promises';
|
|
2
4
|
import { dirname } from 'node:path';
|
|
3
5
|
/** A session id as Claude Code names a transcript: nothing that could be a path or carry anything else. */
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
/** Anything but these is not a name this store wrote: a project's key is turned into one before it is used. */
|
|
6
|
+
const UNSAFE = /[^A-Za-z0-9._-]/g;
|
|
7
|
+
/**
|
|
8
|
+
* The running page server of each project (`2026-10-02-a-page-not-a-file.md` PF2), in the person's own agentwhy
|
|
9
|
+
* folder: one small file a project, readable by its owner alone, since the address holds the page's token. Never
|
|
10
|
+
* throws: a record that cannot be read or written is no record, and a new server is started instead.
|
|
11
|
+
*/
|
|
12
|
+
export class FilePageServers {
|
|
13
|
+
#directory;
|
|
14
|
+
constructor(agentwhyDirectory) {
|
|
15
|
+
this.#directory = join(agentwhyDirectory, 'servers');
|
|
16
|
+
}
|
|
17
|
+
async read(project) {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(await readFile(this.#fileFor(project), 'utf8'));
|
|
20
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async write(project, record) {
|
|
27
|
+
try {
|
|
28
|
+
await mkdir(this.#directory, { recursive: true, mode: 0o700 });
|
|
29
|
+
await writeFile(this.#fileFor(project), JSON.stringify(record), { encoding: 'utf8', mode: 0o600 });
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// No record is a server that is not reused: the next open starts its own.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async remove(project, pid) {
|
|
36
|
+
const known = await this.read(project);
|
|
37
|
+
if (known?.pid !== pid)
|
|
38
|
+
return;
|
|
39
|
+
await rm(this.#fileFor(project), { force: true }).catch(() => undefined);
|
|
40
|
+
}
|
|
41
|
+
#fileFor(project) {
|
|
42
|
+
const safe = project.replace(UNSAFE, '-').slice(0, 200);
|
|
43
|
+
return join(this.#directory, `${safe === '' ? 'project' : safe}.json`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isRecord(value) {
|
|
47
|
+
if (value === null || typeof value !== 'object')
|
|
48
|
+
return false;
|
|
49
|
+
const record = value;
|
|
50
|
+
return typeof record.url === 'string' && Number.isInteger(record.pid) && typeof record.startedAt === 'number';
|
|
51
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
/**
|
|
5
|
+
* agentwhy started again as a process of its own (`2026-10-02-a-page-not-a-file.md` PF3): detached, its streams closed,
|
|
6
|
+
* not waited for - so the command that started it returns at once, and the page server it runs outlives it. Measured on
|
|
7
|
+
* 2026-10-02 (PFB1): such a process outlived its command in Claude Code in VS Code and in the desktop app, and in Codex
|
|
8
|
+
* when the command ran outside its sandbox.
|
|
9
|
+
*/
|
|
10
|
+
export class NodeBackgroundRun {
|
|
11
|
+
#node;
|
|
12
|
+
#script;
|
|
13
|
+
/**
|
|
14
|
+
* The processes this started that have ended, as Node says when it reaps one: an ended child's id may be given to
|
|
15
|
+
* another process, so asking the system by id would not tell them apart.
|
|
16
|
+
*/
|
|
17
|
+
#ended = new Set();
|
|
18
|
+
constructor(node, script) {
|
|
19
|
+
this.#node = node;
|
|
20
|
+
this.#script = script;
|
|
21
|
+
}
|
|
22
|
+
start(args) {
|
|
23
|
+
if (this.#script === undefined)
|
|
24
|
+
return Promise.resolve(undefined);
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
try {
|
|
27
|
+
const child = spawn(this.#node, [this.#script, ...args], { detached: true, stdio: 'ignore' });
|
|
28
|
+
child.once('error', () => resolve(undefined));
|
|
29
|
+
child.once('spawn', () => {
|
|
30
|
+
const { pid } = child;
|
|
31
|
+
if (pid !== undefined)
|
|
32
|
+
child.once('exit', () => this.#ended.add(pid));
|
|
33
|
+
child.unref();
|
|
34
|
+
resolve(pid);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
resolve(undefined);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
running(pid) {
|
|
43
|
+
return !this.#ended.has(pid);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
1
4
|
import { createReadStream } from 'node:fs';
|
|
2
|
-
import { mkdir, open, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { chmod, lstat, mkdir, open, readdir, readFile, readlink, realpath, rename, stat, unlink, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
7
|
import { FileAccessError } from '../ports/file-access-error.js';
|
|
4
8
|
/**
|
|
5
9
|
* The owner alone, for everything this writes. What it writes is a report: which protected files an agent
|
|
@@ -16,6 +20,26 @@ export class NodeFileSystem {
|
|
|
16
20
|
async writeText(path, text) {
|
|
17
21
|
await attempt(path, () => writeFile(path, text, { mode: OWNER_ONLY_FILE }));
|
|
18
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* AO15: the text goes to a temporary file beside the real target - a link followed, so a dotfiles link stays a link -
|
|
25
|
+
* with the target's mode, and then takes its place in one rename. A failure removes the temporary file and leaves the
|
|
26
|
+
* target as it was.
|
|
27
|
+
*/
|
|
28
|
+
async replaceText(path, text) {
|
|
29
|
+
const target = await attempt(path, () => realTarget(path));
|
|
30
|
+
const mode = await stat(target).then((found) => found.mode & 0o777, () => OWNER_ONLY_FILE);
|
|
31
|
+
const temporary = join(dirname(target), `.${basename(target)}.agentwhy-${randomBytes(6).toString('hex')}.tmp`);
|
|
32
|
+
try {
|
|
33
|
+
await attempt(path, () => writeFile(temporary, text, { mode, flag: 'wx' }));
|
|
34
|
+
// The mode a file is created with is cut by the process's umask; the target's own is set again before the rename.
|
|
35
|
+
await attempt(path, () => chmod(temporary, mode));
|
|
36
|
+
await attempt(path, () => rename(temporary, target));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
await unlink(temporary).catch(() => undefined);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
19
43
|
async ensureDirectory(path) {
|
|
20
44
|
await attempt(path, () => mkdir(path, { recursive: true, mode: OWNER_ONLY_DIRECTORY }));
|
|
21
45
|
}
|
|
@@ -88,6 +112,23 @@ function entryKind(entry) {
|
|
|
88
112
|
return 'directory';
|
|
89
113
|
return 'other';
|
|
90
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* The file a path names, every link followed; a file not there yet is named by its folder's real path. A link whose
|
|
117
|
+
* target is not there yet is followed to that target, so the write creates it and the link stays a link.
|
|
118
|
+
*/
|
|
119
|
+
async function realTarget(path) {
|
|
120
|
+
try {
|
|
121
|
+
return await realpath(path);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
|
|
125
|
+
throw error;
|
|
126
|
+
const linked = await lstat(path).then((found) => found.isSymbolicLink(), () => false);
|
|
127
|
+
if (linked)
|
|
128
|
+
return realTarget(resolve(dirname(path), await readlink(path)));
|
|
129
|
+
return join(await realpath(dirname(path)), basename(path));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
91
132
|
async function attempt(path, operation) {
|
|
92
133
|
try {
|
|
93
134
|
return await operation();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { request } from 'node:http';
|
|
4
|
+
/** The computer's own addresses: a record that named anything else was not written by `start`, and is not asked. */
|
|
5
|
+
const LOOPBACK = new Set(['127.0.0.1', 'localhost', '[::1]']);
|
|
6
|
+
/**
|
|
7
|
+
* Asks a page once (`2026-10-02-a-page-not-a-file.md` PF3): an answer of 200 is a server that is running and this page
|
|
8
|
+
* is one it serves. Asking also brings a served page up to date (R75), so a conversation begun since the server started
|
|
9
|
+
* has its report by the time it is opened - and that takes as long as reading it does. Found by review: 3 s took a
|
|
10
|
+
* running server busy with a long conversation for a dead one, and started a second beside it.
|
|
11
|
+
*/
|
|
12
|
+
export class NodePageProbe {
|
|
13
|
+
#timeoutMs;
|
|
14
|
+
constructor(timeoutMs = 10_000) {
|
|
15
|
+
this.#timeoutMs = timeoutMs;
|
|
16
|
+
}
|
|
17
|
+
answers(url) {
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = new URL(url);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return Promise.resolve(false);
|
|
24
|
+
}
|
|
25
|
+
if (parsed.protocol !== 'http:' || !LOOPBACK.has(parsed.hostname))
|
|
26
|
+
return Promise.resolve(false);
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
const asking = request(parsed, { method: 'GET', timeout: this.#timeoutMs }, (response) => {
|
|
29
|
+
response.resume();
|
|
30
|
+
resolve(response.statusCode === 200);
|
|
31
|
+
});
|
|
32
|
+
asking.on('error', () => resolve(false));
|
|
33
|
+
asking.on('timeout', () => {
|
|
34
|
+
asking.destroy();
|
|
35
|
+
resolve(false);
|
|
36
|
+
});
|
|
37
|
+
asking.end();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
/** How much of standard error is kept to say why a trial failed: its first line, cut short. */
|
|
5
|
+
const STDERR_KEPT = 300;
|
|
6
|
+
/**
|
|
7
|
+
* `2026-10-02-codex-approves-its-own-hook.md` AO14, AOB8: a command line run as Codex runs a hook on macOS and Linux -
|
|
8
|
+
* `$SHELL -lc "<command>"`, else `/bin/sh` - with its input on standard input, from the given folder. On Windows
|
|
9
|
+
* Codex's way is not measured, so nothing is tried. The environment is this process's, not Codex's: what passes here
|
|
10
|
+
* is what can be promised, and no more. A timeout ends the shell; a process it started may outlive it briefly, which
|
|
11
|
+
* a trial accepts - the answer is already "timed out".
|
|
12
|
+
*/
|
|
13
|
+
export class ShellCommandTrial {
|
|
14
|
+
#options;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.#options = options;
|
|
17
|
+
}
|
|
18
|
+
run(trial) {
|
|
19
|
+
if (this.#options.platform === 'win32')
|
|
20
|
+
return Promise.resolve({ kind: 'not-tried' });
|
|
21
|
+
const shell = this.#options.shell === undefined || this.#options.shell === '' ? '/bin/sh' : this.#options.shell;
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
let settled = false;
|
|
24
|
+
let timer;
|
|
25
|
+
const done = (outcome) => {
|
|
26
|
+
if (settled)
|
|
27
|
+
return;
|
|
28
|
+
settled = true;
|
|
29
|
+
if (timer !== undefined)
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
resolve(outcome);
|
|
32
|
+
};
|
|
33
|
+
const child = spawn(shell, ['-lc', trial.commandLine], {
|
|
34
|
+
cwd: trial.folder,
|
|
35
|
+
env: { ...this.#options.environment },
|
|
36
|
+
stdio: ['pipe', 'ignore', 'pipe'],
|
|
37
|
+
});
|
|
38
|
+
let stderr = '';
|
|
39
|
+
child.stderr?.on('data', (chunk) => {
|
|
40
|
+
if (stderr.length < STDERR_KEPT)
|
|
41
|
+
stderr += chunk.toString('utf8');
|
|
42
|
+
});
|
|
43
|
+
timer = setTimeout(() => {
|
|
44
|
+
if (settled)
|
|
45
|
+
return;
|
|
46
|
+
child.kill('SIGKILL');
|
|
47
|
+
done({ kind: 'timed-out' });
|
|
48
|
+
}, trial.timeoutMs);
|
|
49
|
+
child.on('error', (error) => done({ kind: 'not-started', reason: error.message }));
|
|
50
|
+
child.on('close', (code, signal) => {
|
|
51
|
+
if (code === null)
|
|
52
|
+
return done({ kind: 'not-started', reason: `ended by ${signal ?? 'a signal'}` });
|
|
53
|
+
done({ kind: 'ran', exitCode: code, stderr: (stderr.split('\n')[0] ?? '').slice(0, STDERR_KEPT) });
|
|
54
|
+
});
|
|
55
|
+
// A command that exits before reading leaves a closed pipe; what it did is read from its exit, never this write.
|
|
56
|
+
child.stdin?.on('error', () => { });
|
|
57
|
+
child.stdin?.end(trial.input);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* The lines drawn above the first question a command asks, and above none of the ones after it. `init` asks up to
|
|
3
5
|
* three things in a row; the mark belongs at the top of that conversation, not before every prompt. One banner is
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { FileAccessError } from './file-access-error.js';
|
|
4
|
+
/** Whether a folder is there: an error the port names is "no", and nothing else is swallowed. */
|
|
5
|
+
export async function isDirectory(directories, path) {
|
|
6
|
+
try {
|
|
7
|
+
return (await directories.kindOf(path)) === 'directory';
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (!(error instanceof FileAccessError))
|
|
11
|
+
throw error;
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/ports/mark-store.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* What a person says they did about a file (R31; `for-people-who-build-with-ai.md` F25, F50): the keys it held were
|
|
3
5
|
* replaced, a template's values are placeholders, what could be done about a file whose contents cannot be changed was
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { isAbsolute, join } from 'node:path';
|
|
2
4
|
import { parsePreToolUseInput } from '../adapter/claude-code/hooks/pre-tool-use-input.js';
|
|
3
|
-
import {
|
|
5
|
+
import { refuseProjectAbove, refuseRulesOf } from '../adapter/claude-code/settings/refuse-project.js';
|
|
4
6
|
import { parseCodexPreToolUseInput } from '../adapter/codex/hooks/pre-tool-use-input.js';
|
|
5
7
|
import { protectedPathsInCommand } from '../core/access/protected-access.js';
|
|
6
|
-
import { FileAccessError } from '../ports/file-access-error.js';
|
|
7
8
|
import { choosePolicy } from '../report/choose-policy.js';
|
|
8
9
|
import { blockingOnly } from '../core/policy/policy.js';
|
|
9
10
|
import { protectedFileReached } from './command-reach.js';
|
|
10
|
-
/** How many folders above the session's the project is looked for in: deeper than any project, short of a loop. */
|
|
11
|
-
const MAX_PROJECT_DEPTH = 64;
|
|
12
11
|
/** A command line is a few kilobytes; past this the input is not held at all, and the command is not checked. */
|
|
13
12
|
const MAX_INPUT_BYTES = 1024 * 1024;
|
|
14
13
|
/**
|
|
@@ -32,6 +31,10 @@ export class CommandRefusal {
|
|
|
32
31
|
if ('anotherTool' in parsed)
|
|
33
32
|
return { kind: 'allow' };
|
|
34
33
|
const settled = codex ? await this.#inCodexProject(choice, parsed.cwd ?? this.#dependencies.workingDirectory) : choice;
|
|
34
|
+
// AO6: the computer-wide check runs in every Codex session; where no project above runs `refuse`, there are no
|
|
35
|
+
// rules anybody chose here, and the command runs with no message.
|
|
36
|
+
if (settled === 'no-project')
|
|
37
|
+
return { kind: 'allow' };
|
|
35
38
|
if (settled === undefined)
|
|
36
39
|
return { kind: 'not-checked', reason: 'project' };
|
|
37
40
|
const chosen = await choosePolicy(settled, this.#dependencies.files, this.#dependencies.tell);
|
|
@@ -55,34 +58,20 @@ export class CommandRefusal {
|
|
|
55
58
|
return { kind: 'refuse', path: reach.path, pattern: reach.pattern, others: 0, route };
|
|
56
59
|
}
|
|
57
60
|
/**
|
|
58
|
-
* CK3: Codex runs its
|
|
59
|
-
* (CKB6).
|
|
60
|
-
*
|
|
61
|
+
* CK3, AO5, AO6: Codex runs its check in the session's folder, which can lie below the project, and names the
|
|
62
|
+
* project nowhere (CKB6). The project is the nearest folder at or above `folder`, short of the home directory, whose
|
|
63
|
+
* Claude Code settings run `refuse`. With no flag of its own, the check reads the rules that project's `refuse`
|
|
64
|
+
* names, and with no project found it is `'no-project'`: the command runs, silently. A relative `--settings`, as an
|
|
65
|
+
* older project-level entry wrote it, resolves against the same project; none found is `undefined`, and the command
|
|
66
|
+
* runs said to be unchecked (R20). An absolute path or a policy file is read as given.
|
|
61
67
|
*/
|
|
62
68
|
async #inCodexProject(choice, folder) {
|
|
63
69
|
const relative = choice.settingsPath;
|
|
64
|
-
if (
|
|
70
|
+
if (choice.policyPath !== undefined || (relative !== undefined && isAbsolute(relative)))
|
|
65
71
|
return choice;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (up === at)
|
|
71
|
-
return undefined;
|
|
72
|
-
at = up;
|
|
73
|
-
}
|
|
74
|
-
return undefined;
|
|
75
|
-
}
|
|
76
|
-
/** Whether a file is there to read; a folder or anything unreadable is not. */
|
|
77
|
-
async #holds(path) {
|
|
78
|
-
try {
|
|
79
|
-
await this.#dependencies.files.readText(path);
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
catch (error) {
|
|
83
|
-
if (error instanceof FileAccessError)
|
|
84
|
-
return false;
|
|
85
|
-
throw error;
|
|
86
|
-
}
|
|
72
|
+
const found = await refuseProjectAbove(this.#dependencies.files, folder, this.#dependencies.home);
|
|
73
|
+
if (relative !== undefined)
|
|
74
|
+
return found === undefined ? undefined : { ...choice, settingsPath: join(found.project, relative) };
|
|
75
|
+
return found === undefined ? 'no-project' : refuseRulesOf(found);
|
|
87
76
|
}
|
|
88
77
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { HOOK_OUTPUT } from '../../adapter/claude-code/contract/hooks.js';
|
|
2
4
|
const NOT_CHECKED = {
|
|
3
5
|
arguments: 'agentwhy refuse was given arguments it does not recognise, so this command was not checked.',
|
|
4
6
|
input: 'agentwhy refuse could not read the hook input, so this command was not checked.',
|
|
5
7
|
policy: 'agentwhy refuse could not read the policy it was given, so this command was not checked.',
|
|
6
8
|
search: 'agentwhy refuse could not look through everything this search reaches, so this command was not checked.',
|
|
7
|
-
project: 'agentwhy refuse found no
|
|
9
|
+
project: 'agentwhy refuse found no project above this folder whose Claude Code settings run refuse, so it could not read the rules it was pointed at and this command was not checked.',
|
|
8
10
|
};
|
|
9
11
|
/** Said to the user, as a hook's JSON on stdout, when a command ran without being checked (R20). */
|
|
10
12
|
export function notCheckedMessage(reason) {
|