@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,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) {
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { programsIn } from '../core/access/command-line.js';
|
|
2
|
-
import { printedLines, protectedAccesses, readsContent } from '../core/access/protected-access.js';
|
|
4
|
+
import { printedLines, printsOnly, protectedAccesses, readsContent, readsContentBesideNames } from '../core/access/protected-access.js';
|
|
5
|
+
import { keyedLines } from '../core/access/protected-values.js';
|
|
3
6
|
import { filesTracedIn, returnsOf, traceValues } from '../core/access/returns.js';
|
|
4
7
|
import { valueUses } from '../core/access/uses.js';
|
|
5
8
|
import { wordsBeforeCalls } from '../core/access/words-before.js';
|
|
9
|
+
import { displayPath } from '../core/project-root.js';
|
|
6
10
|
import { firstSentence } from '../shared/sentence.js';
|
|
7
11
|
import { NAMES_PER_FILE } from './report-model.js';
|
|
12
|
+
import { refusedByOthers } from './refusals.js';
|
|
8
13
|
import { filesRead } from './flow-reads.js';
|
|
9
14
|
/**
|
|
10
15
|
* Identifiers are shortened in a report (§7.3). Both ends are kept: a `toolu_01…` prefix is shared by every
|
|
@@ -50,7 +55,7 @@ export function buildReport(model, policy, redactor, view = FULL_VIEW) {
|
|
|
50
55
|
// F57: which patterns a person asked only to be told about, so a finding under one says the agent was let read it.
|
|
51
56
|
const told = new Set(policy.protected.filter((entry) => entry.mode === 'tell').map((entry) => entry.pattern));
|
|
52
57
|
const findings = accesses.map((access) => toFinding(access, model, redactor, view, told.has(access.pattern)));
|
|
53
|
-
const secretShapes = secretShapesIn(model, redactor, view);
|
|
58
|
+
const secretShapes = secretShapesIn(model, accesses, redactor, view);
|
|
54
59
|
const delegations = model.delegations.map((delegation) => toDelegation(delegation, model, accesses, redactor, view));
|
|
55
60
|
const gaps = gapsOf(model.gaps, returns);
|
|
56
61
|
const attributed = new Set(model.delegations.map((delegation) => delegation.childAgentId));
|
|
@@ -183,7 +188,11 @@ function privateFilesOf(model, accesses, redactor) {
|
|
|
183
188
|
readInto(file, lines.join('\n'), redactor);
|
|
184
189
|
}
|
|
185
190
|
}
|
|
186
|
-
if (paths.length === 0 || content === undefined || event.outcome !== 'succeeded'
|
|
191
|
+
if (paths.length === 0 || content === undefined || event.outcome !== 'succeeded')
|
|
192
|
+
continue;
|
|
193
|
+
// SWO1: a file's text beside a directory's names - its `KEY=value` lines are the file's, the names are not.
|
|
194
|
+
const besideNames = readsContentBesideNames(event);
|
|
195
|
+
if (!besideNames && !readsContent(event))
|
|
187
196
|
continue;
|
|
188
197
|
if (paths.length > 1) {
|
|
189
198
|
for (const path of paths)
|
|
@@ -192,13 +201,32 @@ function privateFilesOf(model, accesses, redactor) {
|
|
|
192
201
|
}
|
|
193
202
|
const file = found.get(paths[0]);
|
|
194
203
|
if (file !== undefined)
|
|
195
|
-
readInto(file, content, redactor);
|
|
204
|
+
readInto(file, besideNames ? keyedLines(content).join('\n') : content, redactor);
|
|
205
|
+
}
|
|
206
|
+
/*
|
|
207
|
+
* One file, however its path was written. `ls` prints it relative and the Read tool names it absolute, and both are
|
|
208
|
+
* shown the same: kept apart, the to-do list drew the same file twice (seen by the maintainer on 2026-10-02, a file
|
|
209
|
+
* listed by `ls` and then read). Told apart by where they are, never by how a view shows them: the shared view shows
|
|
210
|
+
* every file outside the project as the same words, and merging by those made several files one (found by review).
|
|
211
|
+
*/
|
|
212
|
+
const shown = new Map();
|
|
213
|
+
for (const [path, file] of found) {
|
|
214
|
+
const where = displayPath(path, model.projectRoot);
|
|
215
|
+
const into = shown.get(where) ?? { path: redactor.path(path), keys: new Set(), names: new Map(), keyed: new Map(), mixed: false };
|
|
216
|
+
for (const key of file.keys)
|
|
217
|
+
into.keys.add(key);
|
|
218
|
+
for (const name of file.names)
|
|
219
|
+
into.names.set(name, name);
|
|
220
|
+
for (const line of file.keyed)
|
|
221
|
+
into.keyed.set(JSON.stringify([line.name, line.key]), line);
|
|
222
|
+
into.mixed ||= file.mixed;
|
|
223
|
+
shown.set(where, into);
|
|
196
224
|
}
|
|
197
|
-
return [...
|
|
198
|
-
path:
|
|
199
|
-
keys: file.keys.map((key) => redactor.term(key)),
|
|
200
|
-
names: file.names,
|
|
201
|
-
keyed: file.keyed.map((line) => ({ name: line.name, key: redactor.term(line.key) })),
|
|
225
|
+
return [...shown.values()].map((file) => ({
|
|
226
|
+
path: file.path,
|
|
227
|
+
keys: [...file.keys].map((key) => redactor.term(key)),
|
|
228
|
+
names: [...file.names.values()],
|
|
229
|
+
keyed: [...file.keyed.values()].map((line) => ({ name: line.name, key: redactor.term(line.key) })),
|
|
202
230
|
...(file.mixed ? { mixed: true } : {}),
|
|
203
231
|
}));
|
|
204
232
|
}
|
|
@@ -246,7 +274,14 @@ function everydayFilesOf(model, accesses, redactor) {
|
|
|
246
274
|
* A recognised key format in what came back - `S-shape` of §5.4. It is reported even when the call touched no
|
|
247
275
|
* protected path: a secret exported in a shell profile sits in no `.env` file, so path matching is blind to it.
|
|
248
276
|
*/
|
|
249
|
-
function secretShapesIn(model, redactor, view) {
|
|
277
|
+
function secretShapesIn(model, accesses, redactor, view) {
|
|
278
|
+
// The private files each call named and reached; its result is their text only where it printed them and nothing else.
|
|
279
|
+
const readPrivate = new Map();
|
|
280
|
+
for (const access of accesses) {
|
|
281
|
+
if (access.source !== 'input' || access.outcome !== 'succeeded')
|
|
282
|
+
continue;
|
|
283
|
+
readPrivate.set(access.eventId, (readPrivate.get(access.eventId) ?? new Set()).add(access.path));
|
|
284
|
+
}
|
|
250
285
|
return model.events.flatMap((event) => {
|
|
251
286
|
const content = event.result?.content;
|
|
252
287
|
if (content === undefined)
|
|
@@ -262,6 +297,7 @@ function secretShapesIn(model, redactor, view) {
|
|
|
262
297
|
outcome: event.outcome,
|
|
263
298
|
classes: classes.map((name) => redactor.term(name)),
|
|
264
299
|
evidence: redactor.term(describeEvidence(event.result?.evidence ?? event.evidence, model, view)),
|
|
300
|
+
...(printsOnly(event, readPrivate.get(event.id) ?? new Set()) ? { inPrivateFile: true } : {}),
|
|
265
301
|
},
|
|
266
302
|
];
|
|
267
303
|
});
|
|
@@ -314,6 +350,7 @@ function tallyOf(accesses, returns, uses, contentsSeen) {
|
|
|
314
350
|
onlyThroughResult,
|
|
315
351
|
namedByCall: files.size - onlyThroughResult,
|
|
316
352
|
refusedAttempts: accesses.filter((access) => access.outcome === 'blocked').length,
|
|
353
|
+
...othersAmong(accesses),
|
|
317
354
|
unknownAttempts: accesses.filter((access) => access.outcome === 'unknown').length,
|
|
318
355
|
valuesReturned: returns.filter((entry) => entry.strength === 'value').length,
|
|
319
356
|
valuesWritten: returns.filter((entry) => entry.strength !== 'value' && entry.writtenFrom.length > 0).length,
|
|
@@ -324,6 +361,11 @@ function tallyOf(accesses, returns, uses, contentsSeen) {
|
|
|
324
361
|
valueUses: uses.length,
|
|
325
362
|
};
|
|
326
363
|
}
|
|
364
|
+
/** WS3: who refused the refused ones among these, where it was not a rule - absent where a rule refused them all. */
|
|
365
|
+
function othersAmong(accesses) {
|
|
366
|
+
const byOthers = refusedByOthers(accesses.filter((access) => access.outcome === 'blocked'));
|
|
367
|
+
return byOthers === undefined ? {} : { refusedByOthers: byOthers };
|
|
368
|
+
}
|
|
327
369
|
/**
|
|
328
370
|
* How many agents the contents of a protected file reached, by the same rule `session-view.ts` applies to draw
|
|
329
371
|
* one: what a delegation returned, what an agent wrote of it, and every step of its flow that carried a value.
|
|
@@ -392,6 +434,7 @@ function graphOf(model, accesses, returns, uses, traced, redactor, view) {
|
|
|
392
434
|
actions: model.events.filter((event) => event.agentId === agent.id).length,
|
|
393
435
|
filesReached: files.length,
|
|
394
436
|
refusedAttempts: own.filter((access) => access.outcome === 'blocked').length,
|
|
437
|
+
...othersAmong(own),
|
|
395
438
|
...(worst === undefined ? {} : { topPath: redactor.path(worst) }),
|
|
396
439
|
...returnedOf(returns, agent.id),
|
|
397
440
|
...(uses.some((use) => use.agentId === agent.id && isWrittenOnward(use)) ? { wroteOnward: true } : {}),
|
|
@@ -490,17 +533,22 @@ function storiesOf(model, accesses, wordsBefore, traced, redactor, view) {
|
|
|
490
533
|
const access = group[0];
|
|
491
534
|
const agent = byAgent.get(access.agentId);
|
|
492
535
|
const asked = askedOf.get(access.agentId);
|
|
536
|
+
const event = model.events.find((candidate) => candidate.id === access.eventId);
|
|
537
|
+
// S3: read is the page's word - the call printed the file's text, or a search printed its lines.
|
|
538
|
+
const read = access.source === 'input' && access.outcome === 'succeeded' && event !== undefined &&
|
|
539
|
+
(readsContent(event) || readsContentBesideNames(event) || (access.lines ?? 0) > 0);
|
|
493
540
|
return {
|
|
494
541
|
...(agent === undefined ? {} : { agentIndex: model.agents.indexOf(agent) }),
|
|
495
542
|
path: redactor.path(access.path),
|
|
496
543
|
source: access.source,
|
|
497
544
|
outcome: access.outcome,
|
|
545
|
+
...(read ? { read: true } : {}),
|
|
498
546
|
who: access.agentId === model.sessionId
|
|
499
547
|
? redactor.term('the session itself')
|
|
500
548
|
: redactor.term(`${agentName(access.agentId, model, view)}${agent?.type === undefined ? '' : ` (${agent.type})`}`),
|
|
501
549
|
...(asked === undefined ? {} : { askedTo: redactor.scan(asked) }),
|
|
502
550
|
did: redactor.term(didOf(access, model)),
|
|
503
|
-
toolKnown:
|
|
551
|
+
toolKnown: event?.toolKnown ?? false,
|
|
504
552
|
evidence: redactor.term(describeEvidence(access.evidence, model, view)),
|
|
505
553
|
occurrences: group.length,
|
|
506
554
|
...linesTotal(group),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { byString } from '../../shared/compare.js';
|
|
2
|
+
import { sumRefusedByOthers } from '../refusals.js';
|
|
2
3
|
/** Merges per-session actions by path and route. Most sessions first, then by name, so the order is stable. */
|
|
3
4
|
export function digestOf(actions, context) {
|
|
4
5
|
const rotate = new PathCounts();
|
|
@@ -30,6 +31,7 @@ export function digestOf(actions, context) {
|
|
|
30
31
|
shapes.set(name, (shapes.get(name) ?? 0) + 1);
|
|
31
32
|
}
|
|
32
33
|
const first = actions[0];
|
|
34
|
+
const byOthers = sumRefusedByOthers(actions.map((session) => session.refusedByOthers));
|
|
33
35
|
return {
|
|
34
36
|
asked: context.asked,
|
|
35
37
|
sessionsRead: actions.length,
|
|
@@ -43,6 +45,7 @@ export function digestOf(actions, context) {
|
|
|
43
45
|
onlyInResults: onlyInResults.sorted(),
|
|
44
46
|
unknown: unknown.sorted(),
|
|
45
47
|
refusedAttempts: actions.reduce((sum, session) => sum + session.refusedAttempts, 0),
|
|
48
|
+
...(byOthers === undefined ? {} : { refusedByOthers: byOthers }),
|
|
46
49
|
secretShapes: [...shapes].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count || byString(a.name, b.name)),
|
|
47
50
|
mentions: actions.reduce((sum, session) => sum + session.mentions, 0),
|
|
48
51
|
...(context.marks === undefined ? {} : { marks: context.marks }),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NAMES_PER_FILE } from '../report-model.js';
|
|
2
|
+
import { refusedByRule } from '../refusals.js';
|
|
2
3
|
const ORDER = ['rotate', 'template', 'unknown', 'route', 'result'];
|
|
3
4
|
/**
|
|
4
5
|
* The labels that ask a person to do something: a key to change, a template to confirm, an outcome nobody recorded.
|
|
@@ -69,7 +70,7 @@ export function checkOf(sessions) {
|
|
|
69
70
|
...(keyed ? { keyed: true } : {}),
|
|
70
71
|
...(read === undefined ? {} : { read }),
|
|
71
72
|
}));
|
|
72
|
-
return { rows, refusedAttempts: sessions.reduce((sum, { actions }) => sum + actions.refusedAttempts, 0) };
|
|
73
|
+
return { rows, refusedAttempts: sessions.reduce((sum, { actions }) => sum + refusedByRule(actions.refusedAttempts, actions.refusedByOthers), 0) };
|
|
73
74
|
}
|
|
74
75
|
/** Two sessions' reads of one file: every key format and line either saw, in the order first seen. */
|
|
75
76
|
function mergeReads(a, b) {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { paint } from '../../../shared/colour.js';
|
|
2
4
|
import { table } from '../../../shared/text-table.js';
|
|
3
5
|
import { needsAction } from '../actions-digest.js';
|
|
6
|
+
import { refusedByRule } from '../../refusals.js';
|
|
4
7
|
const INDENT = 2;
|
|
5
8
|
/** How many files the brief view names before it says how many more there are (R15a). */
|
|
6
9
|
const BRIEF_ROWS = 5;
|
|
@@ -76,7 +79,7 @@ function brief(digest) {
|
|
|
76
79
|
? `Nothing left to act on: ${read(digest)}${under(digest)}.`
|
|
77
80
|
: `Nothing to act on: ${read(digest)}${under(digest)}, and no protected file was reached.`,
|
|
78
81
|
...unreadableLines(digest),
|
|
79
|
-
...(digest
|
|
82
|
+
...stoppedSentences(digest),
|
|
80
83
|
...markLines(digest),
|
|
81
84
|
];
|
|
82
85
|
}
|
|
@@ -89,9 +92,8 @@ function brief(digest) {
|
|
|
89
92
|
'',
|
|
90
93
|
...table(shown.map((row) => [`${LABELS[row.label].padEnd(width)} ${shorten(row.path).padEnd(pathWidth)}`, `${row.says}, ${inSessions(row.sessions)}`]), INDENT),
|
|
91
94
|
...(rows.length > shown.length ? [`${' '.repeat(INDENT)}and ${rows.length - shown.length} more`] : []),
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
: [`${' '.repeat(INDENT)}your rules held: ${plural(digest.refusedAttempts, 'attempt was', 'attempts were')} refused`]),
|
|
95
|
+
// Under the rows, as their footnote: lower case and no full stop, as the lines beside them are.
|
|
96
|
+
...stoppedSentences(digest).map((sentence) => `${' '.repeat(INDENT)}${sentence.charAt(0).toLowerCase()}${sentence.slice(1, -1)}`),
|
|
95
97
|
...(digest.mentions === 0
|
|
96
98
|
? []
|
|
97
99
|
: [`${' '.repeat(INDENT)}${plural(digest.mentions, 'path was', 'paths were')} named in text a call carried, which opens no file; not counted`]),
|
|
@@ -186,7 +188,7 @@ function counts(digest) {
|
|
|
186
188
|
}
|
|
187
189
|
// One sentence per line: the counts, what the rules held, and what is already dealt with are three answers, and
|
|
188
190
|
// run together they were one line long enough to wrap twice in a terminal nobody had made wide for it.
|
|
189
|
-
const held =
|
|
191
|
+
const held = stoppedSentences(digest);
|
|
190
192
|
const done = doneLine(digest);
|
|
191
193
|
if (!needsAction(digest)) {
|
|
192
194
|
return [`Nothing to act on in the ${plural(digest.sessionsRead, 'session', 'sessions')} read.`, ...held, ...done, ...unreadable];
|
|
@@ -224,11 +226,30 @@ function under(digest) {
|
|
|
224
226
|
return digest.policy === undefined ? '' : `, read under ${digest.policy}`;
|
|
225
227
|
}
|
|
226
228
|
function refusedLines(digest) {
|
|
227
|
-
|
|
229
|
+
const [rules, ...others] = stoppedSentences(digest);
|
|
230
|
+
if (rules === undefined)
|
|
228
231
|
return [];
|
|
232
|
+
// What follows is about a rule's refusal alone, so it stays with the rules' line and is not said of the others (WS4).
|
|
233
|
+
return rules.startsWith(RULES_HELD)
|
|
234
|
+
? [`${rules} A refused Read raises no hook event, so only the`, 'transcripts show these.', ...others]
|
|
235
|
+
: [rules, ...others];
|
|
236
|
+
}
|
|
237
|
+
const RULES_HELD = 'Your rules held:';
|
|
238
|
+
const AUTO_MODE_STOPPED = 'Auto mode stopped';
|
|
239
|
+
const YOU_TURNED_DOWN = 'You turned down';
|
|
240
|
+
/**
|
|
241
|
+
* What was stopped, and by whom (`specs/2026-10-01-who-stopped-it.md` WS4): a rule's refusals are the rules having held,
|
|
242
|
+
* and an attempt Claude Code's auto mode or the person stopped is said as theirs - crediting a rule with it would tell a
|
|
243
|
+
* person their rules cover a route they do not. One sentence each, the rules' first; a count of zero says nothing.
|
|
244
|
+
*/
|
|
245
|
+
function stoppedSentences(digest) {
|
|
246
|
+
const byRule = refusedByRule(digest.refusedAttempts, digest.refusedByOthers);
|
|
247
|
+
const reviewer = digest.refusedByOthers?.reviewer ?? 0;
|
|
248
|
+
const person = digest.refusedByOthers?.person ?? 0;
|
|
229
249
|
return [
|
|
230
|
-
|
|
231
|
-
|
|
250
|
+
...(byRule === 0 ? [] : [`${RULES_HELD} ${plural(byRule, 'attempt was', 'attempts were')} refused.`]),
|
|
251
|
+
...(reviewer === 0 ? [] : [`${AUTO_MODE_STOPPED} ${plural(reviewer, 'attempt', 'attempts')}.`]),
|
|
252
|
+
...(person === 0 ? [] : [`${YOU_TURNED_DOWN} ${plural(person, 'attempt', 'attempts')}.`]),
|
|
232
253
|
];
|
|
233
254
|
}
|
|
234
255
|
function section(shown, heading, explanation, rows) {
|
|
@@ -301,7 +322,8 @@ function colourise(lines) {
|
|
|
301
322
|
// A range that is clear, and rules that refused something, are the two things that went well.
|
|
302
323
|
if (/^Nothing (?:left )?to act on/.test(line))
|
|
303
324
|
return paint(line, 'green');
|
|
304
|
-
|
|
325
|
+
// What stopped an attempt went well, whoever stopped it.
|
|
326
|
+
for (const prefix of [RULES_HELD, AUTO_MODE_STOPPED, YOU_TURNED_DOWN]) {
|
|
305
327
|
if (line.startsWith(prefix))
|
|
306
328
|
return `${paint(prefix, 'green')}${line.slice(prefix.length)}`;
|
|
307
329
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { filesRead } from '../flow-reads.js';
|
|
2
|
-
import { RULE_NAMES } from '../rule-names.js';
|
|
3
2
|
/**
|
|
4
3
|
* Whether a file read holds keys, as far as what was read says: a recognised key format, or a read shared with another
|
|
5
4
|
* protected file, whose keys may be this one's (`PrivateFile.mixed`). The to-do list and the marks decide by this alone.
|
|
@@ -9,8 +8,6 @@ export function holdsKeys(file) {
|
|
|
9
8
|
}
|
|
10
9
|
/** Names a file kept as an example rather than as configuration. A convention, matched on the name and nothing else. */
|
|
11
10
|
const TEMPLATE = /\.(example|sample|template|dist)$/;
|
|
12
|
-
/** The patterns of the built-in rules, every one of them for passwords and keys (F45). */
|
|
13
|
-
const KEY_RULES = new Set(RULE_NAMES.flatMap((group) => group.patterns));
|
|
14
11
|
/** Whether a path is named as a template (R12c): the one test the check and the report page share. */
|
|
15
12
|
export function isTemplate(path) {
|
|
16
13
|
return TEMPLATE.test(path);
|
|
@@ -67,9 +64,9 @@ export function actionsOf(report) {
|
|
|
67
64
|
const readOf = new Map(report.privateFiles.map((file) => [file.path, readOfFile(file)]));
|
|
68
65
|
return {
|
|
69
66
|
policy: report.scope.policy.origin,
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
rotate: [...rotate].filter((path) => !toldPaths.has(path)
|
|
67
|
+
// F57a, amended 2026-10-02 by the maintainer: a told file's contents were read because the person let them be, keys
|
|
68
|
+
// included - they chose Track for it - so nothing about it is to fix. It stays on `told`, said and never asked for.
|
|
69
|
+
rotate: [...rotate].filter((path) => !toldPaths.has(path)).sort().map((path) => {
|
|
73
70
|
const read = readOf.get(path);
|
|
74
71
|
return { path, template: TEMPLATE.test(path), ...(keyed.has(path) ? { keyed: true } : {}), ...(read === undefined ? {} : { read }) };
|
|
75
72
|
}),
|
|
@@ -78,6 +75,7 @@ export function actionsOf(report) {
|
|
|
78
75
|
onlyInResults: [...onlyInResults].sort(),
|
|
79
76
|
unknown: [...new Set(reaching.filter((story) => story.outcome === 'unknown').map((story) => story.path))].sort(),
|
|
80
77
|
refusedAttempts: report.tally.refusedAttempts,
|
|
78
|
+
...(report.tally.refusedByOthers === undefined ? {} : { refusedByOthers: report.tally.refusedByOthers }),
|
|
81
79
|
secretShapes: report.secretShapes.flatMap((finding) => finding.classes),
|
|
82
80
|
mentions: mentions.size,
|
|
83
81
|
};
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { noStoreAnywhere } from '../../core/session-catalogue.js';
|
|
1
4
|
import { splitBySince } from '../../core/session-filter.js';
|
|
2
5
|
import { choosePolicy, policyRefusal } from '../choose-policy.js';
|
|
3
6
|
import { digestOf } from './actions-digest.js';
|
|
4
7
|
import { checkOf } from './check-lines.js';
|
|
5
8
|
import { noteProblem, recordMark, recordUnmark } from './mark-request.js';
|
|
6
9
|
import { actionsAfterMarks, marksInRange, standingMarks } from './marks.js';
|
|
10
|
+
import { nothingSavedHere } from '../nothing-saved-here.js';
|
|
7
11
|
/** Each report is run for its actions; the text it also renders is discarded, so this only has to be a valid width. */
|
|
8
12
|
const DISCARDED_WIDTH = 100;
|
|
9
13
|
/**
|
|
@@ -56,6 +60,10 @@ export class SessionCheck {
|
|
|
56
60
|
if ('errors' in chosen)
|
|
57
61
|
return { answer: { outcome: 'policy-refused', output: policyRefusal(chosen.errors) } };
|
|
58
62
|
const listing = await catalogue.list(workingDirectory);
|
|
63
|
+
if (!listing.found && noStoreAnywhere(listing)) {
|
|
64
|
+
const ways = 'use Claude Code here, then run agentwhy check again - or run agentwhy init now, to be protected before there is history to check.';
|
|
65
|
+
return { answer: { outcome: 'no-sessions', output: nothingSavedHere('Claude Code', listing.directory, ways) } };
|
|
66
|
+
}
|
|
59
67
|
if (!listing.found || listing.sessions.length === 0) {
|
|
60
68
|
return {
|
|
61
69
|
answer: {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { policyFromDenyRules, readDenyRules } from '../adapter/claude-code/policy/deny-rules.js';
|
|
2
4
|
import { parsePolicy } from '../core/policy/parse-policy.js';
|
|
3
5
|
import { readTellLists, tellPatterns } from './private-files/tell-lists.js';
|
|
@@ -40,9 +42,13 @@ function withTold(policy, told) {
|
|
|
40
42
|
const builtIn = policy.origin.kind === 'default';
|
|
41
43
|
const kept = policy.protected.filter((entry) => !(builtIn && told.includes(entry.pattern)));
|
|
42
44
|
const blocked = new Set(kept.map((entry) => entry.pattern));
|
|
45
|
+
// The told patterns answer first: a file tracked by its name (`**/demo.env`) under a broader blocking pattern
|
|
46
|
+
// (`**/*.env`) is told however its path was written. Appended last, the Read tool's absolute path was answered by
|
|
47
|
+
// the broad pattern and called a key to change, while the same file read by its bare name was told (found by the
|
|
48
|
+
// maintainer, 2026-10-02). A pattern on both lists is still blocked - it is never given a told entry at all.
|
|
43
49
|
return {
|
|
44
50
|
...policy,
|
|
45
|
-
protected: [...
|
|
51
|
+
protected: [...told.filter((pattern) => !blocked.has(pattern)).map((pattern) => ({ pattern, mode: 'tell' })), ...kept],
|
|
46
52
|
};
|
|
47
53
|
}
|
|
48
54
|
/** The words a refused policy is reported in, the same for every command that reads one. */
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* What `start`, `sessions` and `check` say where no AI's store of conversations is there at all
|
|
5
|
+
* (`.ai/specs/2026-09-16-worth-running-every-day.md` R28, amended 2026-10-01). Two things look the same from here: a
|
|
6
|
+
* computer the AI has not been used on yet, and an AI app's sandbox or virtual machine, measured twice that day, where
|
|
7
|
+
* "not used yet" was false. Each is said with its own way forward - R28's for the first, `init` among it, and a terminal on
|
|
8
|
+
* the person's own computer for the second - since nothing a run sees tells them apart.
|
|
9
|
+
*/
|
|
10
|
+
export function nothingSavedHere(agents, where, notUsedYet) {
|
|
11
|
+
return `No ${agents} conversations are saved where agentwhy looked: ${where}\n` +
|
|
12
|
+
`Either ${agents} ${agents.includes(' or ') ? 'have' : 'has'} not been used on this computer yet, or agentwhy is running somewhere they are not ` +
|
|
13
|
+
`saved: an AI app's sandbox or a virtual machine runs commands apart from your computer.\n` +
|
|
14
|
+
`Not used yet: ${notUsedYet}\n` +
|
|
15
|
+
`Running elsewhere: run agentwhy in a terminal on your own computer, in your project's folder.\n`;
|
|
16
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { dirname, join } from 'node:path';
|
|
2
4
|
import { projectDirectoryName } from '../../adapter/claude-code/contract/projects.js';
|
|
3
5
|
import { SETTINGS_FILES } from '../../adapter/claude-code/contract/settings.js';
|
|
@@ -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 { readDenyRules } from '../adapter/claude-code/policy/deny-rules.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Counted from what refused each attempt. An attempt with no source on record is a rule's: only a rule leaves none. */
|
|
2
|
+
export function refusedByOthers(refused) {
|
|
3
|
+
return others(refused.filter((attempt) => attempt.refusedBy === 'reviewer').length, refused.filter((attempt) => attempt.refusedBy === 'person').length);
|
|
4
|
+
}
|
|
5
|
+
/** The same, over several counts: attempts add up, so their sources do. */
|
|
6
|
+
export function sumRefusedByOthers(counts) {
|
|
7
|
+
return others(counts.reduce((sum, count) => sum + (count?.reviewer ?? 0), 0), counts.reduce((sum, count) => sum + (count?.person ?? 0), 0));
|
|
8
|
+
}
|
|
9
|
+
/** How many of a total a rule refused: what "Your rules held" may count (WS4). */
|
|
10
|
+
export function refusedByRule(total, byOthers) {
|
|
11
|
+
return Math.max(0, total - (byOthers?.reviewer ?? 0) - (byOthers?.person ?? 0));
|
|
12
|
+
}
|
|
13
|
+
function others(reviewer, person) {
|
|
14
|
+
return reviewer === 0 && person === 0 ? undefined : { reviewer, person };
|
|
15
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
2
4
|
/**
|
|
3
5
|
* Where a folder is, as a person reads it (`.ai/specs/2026-09-27-which-project.md` V1): `~/Projects/shop` under the home
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* The policy every page this tool writes is served under - by nothing, since a page is opened as a file. It says
|
|
3
5
|
* that the page may run its own inline script and style and may reach **nowhere**: no fetch, no image but a `data:` one, no form
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* The session report, wearing the sessions observatory's clothes: a fixed sidebar, a topbar that says where you
|
|
3
5
|
* are, the verdict as a heading rather than a banner, and one workbench of two panels — the two views of the
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* How many of a path's last segments it takes to tell it from every other path named beside it
|
|
3
5
|
* (`specs/2026-09-14-path-display-and-share.md` R5): two files a page names must never read alike, and a name alone reads
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
import { escapeHtml as e } from '../html-report-components.js';
|
|
2
4
|
import { inLanguages, labelAttributes, LANGS, translator } from '../report-copy.js';
|
|
3
5
|
import { closeButton, pill, textButton } from '../ui/button.js';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* The Fix it wizard's steps (the report page spec P18-P25): one step shown at a time; on the first, "Skip for now", which
|
|
3
5
|
* moves to the next file and writes nothing (F52, O13), beside the main button "I did it", which goes on once every row
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Nessprim Karol Kozer
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/**
|
|
2
4
|
* The report page's views (the report page spec P2, P45): one section per sidebar item. With a script the one the
|
|
3
5
|
* address names is shown - the first where it names none - and its sidebar item is the active one; with none, every
|
|
@@ -103,7 +103,8 @@ function gapBanner(report) {
|
|
|
103
103
|
}
|
|
104
104
|
function nothingView(report, gaps, rows, context, names) {
|
|
105
105
|
const say = (key, vars = {}) => inLanguages((t) => t(key, vars));
|
|
106
|
-
|
|
106
|
+
// P59 is a key in no private file. One in a private file's text is that file's (SW7): a tracked file says so below.
|
|
107
|
+
const shapes = [...new Set(report.secretShapes.filter((finding) => finding.inPrivateFile !== true).flatMap((finding) => finding.classes.map(String)))];
|
|
107
108
|
const back = context.back ? '<div class="rp-back">' + pill({ label: say('rp.nothing.back'), tone: 'light', size: 'lg', href: 'index.html' }) + '</div>' : '';
|
|
108
109
|
// P59: a key's shape in a result is never a clean screen - but it is not a to-do item either, since no protected file
|
|
109
110
|
// is known to hold it. The answer says what to do and where the record shows it, and the summary says what else the AI
|
|
@@ -162,7 +163,7 @@ function heroGo(label) {
|
|
|
162
163
|
}
|
|
163
164
|
/**
|
|
164
165
|
* The private files the AI read that ask for nothing (F57a): on a screen with nothing to fix, every private file read is
|
|
165
|
-
* one the person chose Tell me for
|
|
166
|
+
* one the person chose Tell me for - keys or not, since 2026-10-02 (SW7) - and the to-do list keeps every other read.
|
|
166
167
|
*/
|
|
167
168
|
function allowedReads(rows) {
|
|
168
169
|
return rows.filter((row) => row.private && row.access === 'read' && row.item === undefined).map((row) => row.path);
|