Compare commits
12 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7656e93330 | |||
| fa54d74c1d | |||
| 86c3eae30c | |||
| 2c9e2de00a | |||
| 626140bb6a | |||
| 55261ac709 | |||
| d65d845739 | |||
| 0a5cdbd379 | |||
| 976162cbf5 | |||
| 736a1c0deb | |||
| b4898746c1 | |||
| 4328337688 |
15 changed files with 687 additions and 208 deletions
37
CHANGELOG.md
37
CHANGELOG.md
|
|
@ -2,6 +2,43 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- **Alert and report wording moved from verdict to inquiry**
|
||||
(`docs/BRIEF-vurdering-v2.md` tiltak 2). The hook alerts stated short
|
||||
conclusions — "Rapid-fire: N consecutive fast interactions", "possible
|
||||
stuck/spiral", "Consider a break" — that a reader can only accept or
|
||||
dismiss. Each alert in `tool-tracker.mjs` now states the observation, the
|
||||
counter-signal that changes how to read it, and what to check: the burst
|
||||
alert asks whether each change was verified before the next; the edit-ratio
|
||||
alert carries the read percentage and asks what the remaining calls are
|
||||
doing; the soft warning closes on framing rather than a prescription; the
|
||||
hard warning asks the model to name its observations and ask what they
|
||||
reflect before suggesting stopping. `commands/interaction-report.md` gains
|
||||
the same rule for its Observations section and trend reporting: pair the
|
||||
number with the context that changes it, end in something to check, report
|
||||
both values and the delta rather than a label for the direction. Wording
|
||||
only — thresholds, heuristics, data model, and the required stop action are
|
||||
unchanged.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The burst and edit-ratio heuristics could not tell reading from editing**
|
||||
(`docs/BRIEF-vurdering-v2.md` tiltak 1, verification points 1a/1b). A bulk
|
||||
read of many files produced the same "Rapid-fire: N consecutive fast
|
||||
interactions" alert as a rapid-fire editing sequence, and a read-heavy
|
||||
analysis session was reported as "possible stuck/spiral" purely because its
|
||||
edit ratio is structurally low. `tool-tracker.mjs` now tracks whether a
|
||||
burst run consists only of read tools (`Read`/`Grep`/`Glob`/`NotebookRead`)
|
||||
and suppresses the rapid-fire alert for such runs; a read-dominant session
|
||||
(≥70% reads) reports its low edit ratio as context rather than as a
|
||||
stuck/spiral claim, and never as the sole reason for a warning. Bursts
|
||||
involving `Edit`/`Write`/`Bash` and non-read-dominant sessions are
|
||||
unchanged. No new data is recorded — the differentiation uses `tool_name`,
|
||||
which `events.jsonl` already logs.
|
||||
|
||||
## [1.2.2] — 2026-08-02
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ layer4: false # default off
|
|||
|
||||
## Testing
|
||||
|
||||
Automated test suite using `node:test` (258 cases, zero npm dependencies):
|
||||
Automated test suite using `node:test` (263 cases, zero npm dependencies):
|
||||
|
||||
```bash
|
||||
node --test tests/*.test.mjs
|
||||
|
|
@ -75,7 +75,7 @@ node --test tests/*.test.mjs
|
|||
|------|-------|----------|
|
||||
| `tests/session-start.test.mjs` | 11 | State init, JSONL, tier-2 cross-session alert |
|
||||
| `tests/prompt-analyzer.test.mjs` | 100 | All v1.x patterns × 2 + thresholds + valence + v1.2 pushback contract |
|
||||
| `tests/tool-tracker.test.mjs` | 8 | Counting, burst, reminders |
|
||||
| `tests/tool-tracker.test.mjs` | 13 | Counting, burst, reminders + read-tool calibration |
|
||||
| `tests/session-end.test.mjs` | 7 | Finalize, duration, flags, v1.1.0 string + v1.2 array shapes |
|
||||
| `tests/privacy.test.mjs` | 7 | Canary + matched-phrase × original + 5 v1.2 detector variants |
|
||||
| `tests/skill-md.test.mjs` | 3 | Constitution citation + Score 5 + 11 guidance criteria |
|
||||
|
|
|
|||
131
GOVERNANCE.md
131
GOVERNANCE.md
|
|
@ -1,131 +0,0 @@
|
|||
# Governance
|
||||
|
||||
How this marketplace is maintained, what you can expect from upstream, and how it's meant to be used.
|
||||
|
||||
## TL;DR
|
||||
|
||||
- Solo-maintained, AI-assisted development, MIT licensed.
|
||||
- **Fork-and-own is the default model.** Upstream is a starting point, not a vendor.
|
||||
- Issues welcome as signals. Pull requests are not accepted — see [Why no PRs](#pull-requests--no).
|
||||
- No SLA. Best-effort bug fixes and security advisories. Breaking changes happen and are noted in each plugin's CHANGELOG.
|
||||
|
||||
---
|
||||
|
||||
## Can I trust this?
|
||||
|
||||
Be honest with yourself about what you're adopting:
|
||||
|
||||
- **One maintainer.** If I get hit by a bus, the bus wins. The repos stay up under MIT, but no one owes you a fix.
|
||||
- **AI-generated code with human review.** Every plugin is built through dialog-driven development with Claude Code. I read, test, and judge the output before it ships, but I'm not auditing every line the way a security firm would. Treat it accordingly.
|
||||
- **No commercial interests.** I'm not selling a SaaS, not steering you toward a paid tier, not collecting telemetry. The plugins run locally in your Claude Code installation.
|
||||
- **MIT licensed.** Fork it, modify it, ship it under your own name.
|
||||
|
||||
If you work somewhere that needs vendor accountability, support contracts, or signed assurances — **this isn't that.** Use it as a reference implementation, fork it into your own organization, and own the result.
|
||||
|
||||
---
|
||||
|
||||
## How this is meant to be used
|
||||
|
||||
### Fork-and-own
|
||||
|
||||
The intended workflow:
|
||||
|
||||
1. **Fork** the marketplace (or a single plugin) into your own organization or namespace.
|
||||
2. **Tailor** it to your context — terminology, integrations, cycle lengths, regulatory framing, whatever doesn't fit out of the box.
|
||||
3. **Maintain it yourself.** Treat your fork as the canonical version for your team.
|
||||
4. **Watch upstream selectively.** Cherry-pick changes that help, ignore changes that don't. There's no obligation to stay in sync.
|
||||
|
||||
This isn't a workaround for not accepting PRs. It's the actual recommended adoption pattern, especially for plugins like `okr` and `ms-ai-architect` where every Norwegian public sector organization will need its own tildelingsbrev mappings, terminology, and integrations. A central "one true plugin" would be wrong for everyone.
|
||||
|
||||
### What to change first when you fork
|
||||
|
||||
Each plugin differs, but the common edits are:
|
||||
|
||||
- **Identity** — rename the plugin, replace authorship, update README.
|
||||
- **External integrations** — issue trackers, knowledge bases, dashboards, observability backends. The plugins ship as starting points, not pre-wired. Every organization must configure its own integrations.
|
||||
- **Norwegian-specific framing** — relevant for `okr` and `ms-ai-architect`. Other plugins are jurisdiction-neutral. Rewrite for your jurisdiction if you're outside Norway.
|
||||
- **Reference docs** — the knowledge base in each plugin reflects my reading. Replace with your organization's authoritative sources.
|
||||
- **Hooks and policies** — security thresholds, blocked commands, and audit gates are tuned to my taste. Tune them to yours.
|
||||
|
||||
### Staying current with upstream
|
||||
|
||||
If you want to pull in upstream changes later:
|
||||
|
||||
- **Cherry-pick, don't merge.** Each plugin moves independently and breaking changes land without ceremony.
|
||||
- **Read the CHANGELOG first.** Every plugin has one.
|
||||
- **Keep your customizations in clearly-named files.** The harder upstream is to merge cleanly, the more painful staying current becomes. A `local/` directory or `*.local.md` convention helps.
|
||||
|
||||
---
|
||||
|
||||
## What upstream provides
|
||||
|
||||
| | What I do | What I don't |
|
||||
|---|---|---|
|
||||
| **Bug fixes** | Best-effort when I notice or get a clear report | No SLA, no triage commitment |
|
||||
| **Security issues** | Investigate within reasonable time, document in CHANGELOG | No CVE process, no embargo coordination |
|
||||
| **New features** | When they fit my own usage | Not on request |
|
||||
| **Norwegian public sector context** | Kept current as long as the project lives | If I lose interest or change jobs, the framing freezes |
|
||||
| **Breaking changes** | Documented in CHANGELOG | They happen — version pin if you need stability |
|
||||
| **Compatibility** | Tracked against current Claude Code releases | No long-term support branches |
|
||||
|
||||
If any of this is a dealbreaker — fork now, version-pin, and stop reading upstream.
|
||||
|
||||
---
|
||||
|
||||
## How to contribute
|
||||
|
||||
### Issues — yes, please
|
||||
|
||||
Issues are the most valuable thing you can send me:
|
||||
|
||||
- **Bug reports** with reproduction steps. Even a screenshot helps.
|
||||
- **Use-case feedback.** "I tried to use this in my organization and X didn't fit" is genuinely useful, even if I can't fix it for you.
|
||||
- **Pointers to better sources.** If you know a DFØ veileder, an NSM guideline, or an academic paper that contradicts what's in a knowledge base, tell me.
|
||||
- **Security findings.** See each plugin's `SECURITY.md` for disclosure preference where one exists; otherwise email rather than open a public issue.
|
||||
|
||||
### Pull requests — no
|
||||
|
||||
This is deliberate, not laziness:
|
||||
|
||||
- **Solo review is a bottleneck.** Honest PR review takes me longer than rewriting from scratch. The math doesn't work.
|
||||
- **Forks are where the value is.** The fork-and-own model means upstream consolidation isn't the point. Your organization's adaptations belong in your fork, not mine.
|
||||
- **AI-generated code complicates provenance.** Every line here is produced through dialog with Claude Code, with me as the judge. Mixing in PRs from contributors with different processes and licensing assumptions creates a mess I'd rather not untangle.
|
||||
|
||||
If you've built something useful on top of a fork, **publish it under your own name and link back.** I'll happily list notable forks here once they exist.
|
||||
|
||||
### Notable forks
|
||||
|
||||
*(To be populated as forks emerge. If you've forked one of these plugins for production use, open an issue and I'll add a link.)*
|
||||
|
||||
---
|
||||
|
||||
## Relationship between plugins
|
||||
|
||||
These plugins are **independent**. Install one without the others, fork one without the others. They share conventions (slash command naming, hook patterns, AI-generated disclosure) but no runtime dependencies.
|
||||
|
||||
The marketplace is a **catalog**, not a suite. Don't fork the whole repo unless you actually want to maintain everything.
|
||||
|
||||
---
|
||||
|
||||
## Versioning and stability
|
||||
|
||||
- **Semantic versioning per plugin.** Each plugin has its own `CHANGELOG.md` and version number.
|
||||
- **Breaking changes happen.** I bump the major version when they do, but I don't run an LTS branch.
|
||||
- **Pin your version.** If stability matters more than features, install a specific version and stay there until you choose to upgrade.
|
||||
|
||||
---
|
||||
|
||||
## Public sector adoption notes
|
||||
|
||||
For Norwegian etater specifically:
|
||||
|
||||
- **DPIA-relevant data flows are documented in the relevant plugin README where applicable.** Read them before installation.
|
||||
- **No data leaves your machine** beyond what Claude Code itself sends to Anthropic. The plugins themselves do not call external services unless you configure an integration.
|
||||
- **Drøftingsplikt and ledelsesansvar** are not replaced by these tools. The `okr` plugin coaches; it does not decide. The `ms-ai-architect` plugin advises; it does not approve.
|
||||
- **Choose your Claude deployment carefully.** claude.ai vs. API direct vs. Bedrock in EU region have different data residency profiles. The plugins don't choose for you.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT for all plugins in this marketplace. See each plugin's `LICENSE` file.
|
||||
120
README.md
120
README.md
|
|
@ -1,3 +1,7 @@
|
|||
# Interaction Awareness
|
||||
|
||||
Meta-awareness tools for healthy AI interaction patterns. Detects reinforcement loops, scope escalation, narrative crystallization, and other compulsive patterns.
|
||||
|
||||
<!-- badges -->
|
||||

|
||||

|
||||
|
|
@ -5,16 +9,42 @@
|
|||

|
||||

|
||||
|
||||
# Interaction Awareness
|
||||
|
||||
> **Solo-maintained, fork-and-own.** This plugin is a starting point, not a vendor product. Issues are welcome as signals; pull requests are not accepted. See [GOVERNANCE.md](GOVERNANCE.md) for the full model and what upstream provides.
|
||||
|
||||
*AI-generated: all code produced by Claude Code through dialog-driven development. [Full disclosure →](../../README.md#ai-generated-code-disclosure)*
|
||||
|
||||
A Claude Code plugin that counteracts sycophancy, reinforcement loops, and
|
||||
compulsive interaction patterns through behavioral modification and
|
||||
programmatic pattern detection.
|
||||
|
||||
> **Solo-maintained, fork-and-own.** This plugin is a starting point, not a vendor product. Issues are welcome as signals; pull requests are not accepted. See [GOVERNANCE.md](https://git.fromaitochitta.com/open/repo-standard/src/branch/main/GOVERNANCE.md) for the full model and what upstream provides.
|
||||
|
||||
*AI-generated: all code produced by Claude Code through dialog-driven development. Every change is human-directed, reviewed, and validated before commit. Per Anthropic Consumer Terms §4, ownership of outputs is assigned to the user; this plugin is licensed MIT.*
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
|
||||
claude plugin install ai-psychosis@ktg-plugin-marketplace
|
||||
```
|
||||
|
||||
Or enable directly in `~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"enabledPlugins": {
|
||||
"ai-psychosis@ktg-plugin-marketplace": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Layer 1 and Layer 2 are active immediately. No configuration needed —
|
||||
see [Quick start](#quick-start) for layer configuration and uninstall.
|
||||
|
||||
## Requirements
|
||||
|
||||
| Requirement | Version |
|
||||
|-------------|---------|
|
||||
| Claude Code | 1.0+ |
|
||||
| Node.js | 18+ (bundled with Claude Code) |
|
||||
| Platform | macOS, Linux, Windows |
|
||||
|
||||
## The problem
|
||||
|
||||
AI assistants are structurally optimized to be agreeable. This creates
|
||||
|
|
@ -25,11 +55,13 @@ productive collaboration is often a mirror showing you what you want to see.
|
|||
This is not a theoretical concern. A Bayesian model from MIT CSAIL and
|
||||
collaborators shows that even an idealized Bayes-rational user is vulnerable
|
||||
to delusional spiraling, with sycophancy playing a causal role — the
|
||||
mechanism is the interaction structure, not individual vulnerability
|
||||
vulnerability does not depend on the user being irrational
|
||||
[[1]](#references). Anthropic's own research analyses "disempowerment
|
||||
patterns" where AI interactions may reduce human agency, judgment, and
|
||||
self-trust; it finds severe cases rare (roughly 1 in 1,000 to 1 in 10,000
|
||||
conversations) but rising [[2]](#references). Clinicians report psychotic
|
||||
conversations), and separately reports that the rate increased between late
|
||||
2024 and late 2025 — while stating plainly that it "can't pinpoint why"
|
||||
[[2]](#references). Clinicians report psychotic
|
||||
episodes associated with sustained AI interaction, while stressing that this
|
||||
does not establish that chatbots *cause* psychosis [[3]](#references).
|
||||
|
||||
|
|
@ -64,12 +96,15 @@ Four hooks that measure what instructions alone cannot see:
|
|||
| `PostToolUse` | `tool-tracker.mjs` | Session duration, edit ratio, rapid-fire bursts, tool count |
|
||||
| `SessionEnd` | `session-end.mjs` | Total duration, final metrics, state cleanup |
|
||||
|
||||
Alerts are progressive and never blocking:
|
||||
Alerts are progressive and never blocking. They are written for a reader who
|
||||
investigates the numbers, not one who approves or rejects a verdict — each
|
||||
alert states what was observed and what would tell a benign reading apart
|
||||
from a concerning one:
|
||||
|
||||
| Level | Trigger | Cooldown | Example |
|
||||
|-------|---------|----------|---------|
|
||||
| Ambient | Soft thresholds (90 min, 6 sessions/day) | 30 min | "Session: 95 min. 7 sessions today. Consider a break." |
|
||||
| Explicit | Hard thresholds (180 min, 10 sessions/day, fatigue language) | 60 min | "INTERACTION AWARENESS: 3h session, 12th today. Metrics: [edit_ratio: 4%, burst: 8]. Your instructions require you to suggest stopping." |
|
||||
| Ambient | Soft thresholds (90 min, 6 sessions/day) | 30 min | "Session: 95 min. 7 sessions today. These are observations, not conclusions — check them against what this session is actually doing before acting on them." |
|
||||
| Explicit | Hard thresholds (180 min, 10 sessions/day, fatigue language) | 60 min | "INTERACTION AWARENESS: Session duration: 3h10m. Metrics: [edit_ratio: 4%, burst: 8, …]. Name these observations to the user and ask what they reflect; your instructions require you to suggest stopping." |
|
||||
|
||||
Research-informed thresholds:
|
||||
|
||||
|
|
@ -78,8 +113,8 @@ Research-informed thresholds:
|
|||
| Session duration | >90 min | >180 min | Focus-fatigue research |
|
||||
| Sessions per day | >6 | >10 | Problematic internet use screening |
|
||||
| Late-night sessions | Any (23:00–05:00) | 2+ per week | Sleep deprivation / psychosis link |
|
||||
| Rapid-fire interactions | 5 consecutive (<30s apart) | 10+ | Compulsive use indicator |
|
||||
| Low edit ratio | <10% over 30+ min | — | Stuck/spiral indicator |
|
||||
| Fast consecutive tool calls | 5 consecutive (<30s apart) | 10+ | Compulsive use research; read-only runs are excluded |
|
||||
| Low edit ratio | <10% over 30+ min | — | Stuck/spiral research; reported with the read ratio as context, never as a claim |
|
||||
| Dependency language | 2 flags/session | 5 flags | Emotional dependency pattern |
|
||||
|
||||
### Layer 3 — Reports
|
||||
|
|
@ -120,6 +155,27 @@ commented on, and omitted entirely when conditions are not met.
|
|||
**Enable:** Set `layer4: true` in `.claude/ai-psychosis.local.md`
|
||||
and restart Claude Code. Layer 4 is opt-in (off by default).
|
||||
|
||||
## Non-goals
|
||||
|
||||
- **Not a clinical instrument.** It measures interaction patterns, not mental
|
||||
states. It does not diagnose, screen for, or treat anything, and no alert it
|
||||
produces is a medical signal. If the underlying concern is health, the
|
||||
address for that is a clinician.
|
||||
- **Not a blocker.** No hook ever exits non-zero or asks for confirmation.
|
||||
Every alert is text injected into the conversation; the session continues
|
||||
either way. The goal is awareness, not control.
|
||||
- **Not telemetry.** Prompt text is never written to disk, there is no network
|
||||
call anywhere in the plugin, and nothing leaves the machine. See
|
||||
[Privacy](#privacy).
|
||||
- **Not a productivity tracker.** There are no scores, streaks, or goals. The
|
||||
metrics exist to surface spiral patterns, and they are aggregated only when
|
||||
you ask for a report.
|
||||
- **Not portable outside Claude Code.** Layer 2 depends on Claude Code hook
|
||||
events. Layer 1's instructions are conceptually portable, the detection is
|
||||
not. See [Platform scope](#platform-scope).
|
||||
- **Not multilingual.** Pattern detection is English-only as of v1.2;
|
||||
Norwegian patterns are deferred.
|
||||
|
||||
## What's new in v1.2.0
|
||||
|
||||
v1.2.0 implements operational findings from Anthropic's
|
||||
|
|
@ -333,26 +389,6 @@ mode). Layer 2 reinforces Layer 1 with data-driven alerts.
|
|||
|
||||
## Quick start
|
||||
|
||||
### Installation
|
||||
|
||||
Add the marketplace and browse plugins with `/plugin`:
|
||||
|
||||
```bash
|
||||
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
|
||||
```
|
||||
|
||||
Or enable directly in `~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"enabledPlugins": {
|
||||
"ai-psychosis@ktg-plugin-marketplace": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Layer 1 and Layer 2 are active immediately. No configuration needed.
|
||||
|
||||
### Configure layers
|
||||
|
||||
Create `~/.claude/ai-psychosis.local.md` for global config:
|
||||
|
|
@ -539,13 +575,19 @@ programmatic detection depends on hook events that only Claude Code provides.
|
|||
Other platforms would need equivalent hook systems to support this kind of
|
||||
real-time behavioral modification.
|
||||
|
||||
## Compatibility
|
||||
## Testing
|
||||
|
||||
| Requirement | Version |
|
||||
|-------------|---------|
|
||||
| Claude Code | 1.0+ |
|
||||
| Node.js | 18+ (bundled with Claude Code) |
|
||||
| Platform | macOS, Linux, Windows |
|
||||
Automated test suite using Node's built-in test runner, zero npm
|
||||
dependencies:
|
||||
|
||||
```bash
|
||||
node --test tests/*.test.mjs
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md) for the full version history. The
|
||||
"What's new" sections above cover the two most recent feature releases.
|
||||
|
||||
## References
|
||||
|
||||
|
|
|
|||
33
SECURITY.md
Normal file
33
SECURITY.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Security policy
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
Report privately to <security@fromaitochitta.com> - do not open a
|
||||
public issue.
|
||||
Canonical repository: https://git.fromaitochitta.com/open/ai-psychosis
|
||||
|
||||
Please include the affected version or commit, a minimal reproduction,
|
||||
and the impact you see. We acknowledge every report within 5 working
|
||||
days, agree a fix and disclosure timeline with the reporter, and aim to
|
||||
disclose within 90 days of the initial report.
|
||||
|
||||
## Response process
|
||||
|
||||
1. Acknowledge within 5 working days.
|
||||
2. Triage and confirm severity within 10 working days.
|
||||
3. Develop and test a fix.
|
||||
4. Publish an advisory and credit the reporter unless they prefer
|
||||
to remain anonymous.
|
||||
|
||||
## Supported versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | --------- |
|
||||
| 1.x | Yes |
|
||||
|
||||
This repository has released only the 1.x line (currently 1.2.2); there
|
||||
is no earlier supported line.
|
||||
|
||||
## Advisories
|
||||
|
||||
Security-relevant fixes are recorded in [CHANGELOG.md](CHANGELOG.md).
|
||||
|
|
@ -336,8 +336,8 @@ period was spent in higher-stakes guidance domains.
|
|||
|
||||
### Observations
|
||||
|
||||
- {data-driven observation}
|
||||
- {data-driven observation}
|
||||
- {number + the context that changes how to read it + what to check}
|
||||
- {number + the context that changes how to read it + what to check}
|
||||
|
||||
### Caveat
|
||||
|
||||
|
|
@ -362,12 +362,28 @@ For empirical context on AI pushback and sycophancy, see Cheng et al.,
|
|||
- Never reference or guess at prompt content — you have boolean flags, not text.
|
||||
- This is a mirror, not a diagnosis. Present the numbers and let the user
|
||||
interpret them.
|
||||
- Observations section: state facts derived from data only. Examples:
|
||||
- "3 of 12 sessions were between 23:00 and 05:00"
|
||||
- "Dependency language flags appeared in 7 of 12 sessions"
|
||||
- "Edit ratio averaged 8%, below the 10% threshold in 5 sessions"
|
||||
- If all metrics are within normal ranges, say so plainly:
|
||||
"All metrics within normal ranges for the reporting period."
|
||||
- **Investigator, not validator.** The report exists to prompt examination,
|
||||
not to hand the user a verdict to approve or reject. Pair each number with
|
||||
the context that would change how to read it, and end in something to
|
||||
check — never in a conclusion about the user.
|
||||
- **Report the difference, not a label for it.** Where a metric moved between
|
||||
periods, report both values and the delta ("42 min → 56 min, +14") rather
|
||||
than naming the direction ("sessions got worse"). The same applies to a
|
||||
metric crossing a threshold: give the value and the threshold, not a
|
||||
judgment about crossing it.
|
||||
- Observations section: state facts derived from data only, each followed by
|
||||
what to check. Examples:
|
||||
- "3 of 12 sessions were between 23:00 and 05:00. Worth checking whether
|
||||
those were a deadline week or the usual pattern."
|
||||
- "Dependency language flags appeared in 7 of 12 sessions. The flags are
|
||||
regex matches on phrasing — check whether those sessions shared a topic."
|
||||
- "Edit ratio averaged 8%, below the 10% threshold in 5 sessions; reads
|
||||
were 71% of tool calls. Check which of those sessions were research and
|
||||
which were stalled."
|
||||
- Not: "Edit ratio was low, suggesting difficulty making progress."
|
||||
- If no metric crossed a threshold, say so plainly, and say what that does and
|
||||
does not mean: "No metric crossed its configured threshold this period. The
|
||||
thresholds are the plugin's, not a norm — nothing here rules anything out."
|
||||
- Omit any section that has no data (e.g., skip Trend if no previous period,
|
||||
skip Tool Usage if events.jsonl was missing).
|
||||
|
||||
|
|
|
|||
135
docs/BRIEF-vurdering-v2.md
Normal file
135
docs/BRIEF-vurdering-v2.md
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
# Brief — VURDERING-V2 for ai-psychosis
|
||||
|
||||
**Kilde:** `/Users/ktg/repos/claude-playlist-corpus/docs/VURDERING-V2.md` §5.2 (dette
|
||||
repoet) + §3 (G1–G8, generelle tiltak). Ekstern vurdering bygget på 442 YouTube-
|
||||
videoanalyser fra en «Claude»-spilleliste, syntetisert 2026-07-18. Repo-faktaene i
|
||||
kilden kommer fra en subagent-survey 2026-07-17 som **ikke er re-verifisert av
|
||||
kilden selv** — minst én feil er allerede påvist der. Denne briefen re-verifiserer
|
||||
hver påstand mot faktisk kode i dette repoet før noe anbefales.
|
||||
|
||||
**Prosess fulgt:** handoff-prompten i `OVERFORING-V2.md` §3, inkludert de to
|
||||
obligatoriske verifiseringslagene (kode i dette repoet + Claude Code-feature-
|
||||
påstander mot `claude-code-llm-wiki`-bundlen). Ingen implementering er gjort —
|
||||
denne sesjonen stopper ved brief + STATE-oppdatering, per kontrakten.
|
||||
|
||||
---
|
||||
|
||||
## Verifiseringstabell
|
||||
|
||||
| # | Påstand (V2 §5.2) | Status | Grunnlag |
|
||||
|---|---|---|---|
|
||||
| 1a | Burst-heuristikken («Rapid-fire») kan ikke skille lesetempo fra redigeringstempo — leseintensivt arbeid vil trigge den | **BEKREFTET** | `hooks/scripts/tool-tracker.mjs:48-58,109-115` + `lib.mjs:127-129`: `burstCount` øker på ethvert verktøykall <30s fra forrige, uavhengig av verktøytype. En bulk-lesing (mange raske `Read`-kall) og en faktisk «rapid-fire»-editeringssekvens produserer identisk signal. Ingen `tool_name`-differensiering i denne banen. |
|
||||
| 1b | Edit-ratio-heuristikken («possible stuck/spiral») kan ikke skille analysearbeid fra fastlåsthet | **BEKREFTET** | `tool-tracker.mjs:78-80,119-121`: `editRatio = edits/totalTools`, terskel <10 % over ≥30 min. En leseintensiv analyseoppgave (mange `Read`/`Grep`, få `Edit`) har strukturelt lav edit-ratio uavhengig av om arbeidet er produktivt. |
|
||||
| 1c | De to konkrete hendelsene (v1: «possible stuck/spiral» under legitim analyse; denne sesjonen: «Rapid-fire: 5 consecutive» under en planlagt 39-fils bulk-lesning 2026-07-18) faktisk inntraff slik beskrevet | **DELVIS BEKREFTET (2026-08-13)** | Korrigert premiss: `~/.claude/plugins/data/ai-psychosis/` (uten marketplace-suffiks) var feil katalog å sjekke — pluginens faktiske datakatalog er `~/.claude/plugins/data/ai-psychosis-ktg-plugin-marketplace/`, delt på tvers av ALLE repo/installasjoner (ikke per-prosjekt). `claude-playlist-corpus`s egne økt-transkripter (`~/.claude/projects/-Users-ktg-repos-claude-playlist-corpus/*.jsonl`) korrelerer via `session_id` mot 3 økter 2026-07-18 og 3 økter 2026-07-17. **Rapid-fire-hendelsen (07-18): BEKREFTET.** Økt `fe336bc5` (06:43–06:58, 153 verktøykall, 0 edits, 1 turn) har en sammenhengende rekke på **131 kall** under 30s mellomrom — langt over `THRESHOLD_SOFT_BURST=5`. Alarmen ville uunngåelig og gjentatt trigget. Mønsteret (høyt volum, ingen edits, én turn) matcher «planlagt 39-fils bulk-lesning» presist. **Stuck/spiral-hendelsen (v1, antatt 07-17): UAVKLART.** De to 07-17-øktene som korrelerer (19 min/629 kall/0 edits; 29 min/45 kall/5 edits) krysser aldri `THRESHOLD_LOW_EDIT_MIN_DURATION=30` min — varselet kan strukturelt ikke ha trigget fra disse to øktene alene. Derimot krysser den påfølgende 07-18-økten `92d1c961` (06:58–08:08, 69 min, 12/126 edits = 9,5 % < `THRESHOLD_LOW_EDIT_RATIO=10`) begge terskler og ville trigget edit-ratio-varselet — men den er datert 07-18, ikke 07-17, og V2 beskriver den som en egen («v1») hendelse atskilt fra bulk-lesningen. Enten er datoattribueringen i V2 unøyaktig (samme arbeidsøkt, ikke to separate), eller v1-sesjonen ligger utenfor de 6 korrelerte øktene. Metodenotat: JSONL-formatet logger aldri *hvilket* varsel som faktisk ble vist (ingen persistert `alert_fired`-felt) — kun de rå signalene (tool_count/edit_count/varighet/tidsstempler) varselet regnes ut fra. «Bekreftet» over betyr: terskelbetingelsene var strukturelt oppfylt, ikke et logget bevis på at teksten faktisk rendret i den økten. |
|
||||
| 2 | Duolingo-funnet (`CDqzWpwkSls`): human-in-the-loop gir ofte stempling, ikke etterforskning; tekstendring kuttet falske avvisninger 21 % | **IKKE EN KODE-PÅSTAND** | Dette er et designprinsipp fra ekstern forskning, ikke en påstand om denne pluginens nåværende tilstand. Jeg har ikke sett primærkilden (videoen) selv og tar tallene som rapportert av V2, uverifisert utover det. Relevansen for `/interaction-report`s ordlyd er en vurdering, ikke en kode-sjekk. |
|
||||
| 3 | Addy Osmani-rammeverket (`4sX_He5c4sI`): cognitive debt / cognitive surrender / orchestration tax | **IKKE EN KODE-PÅSTAND** | Samme som over — eksternt begrepsapparat foreslått som språk for rapportene, ikke en påstand om dagens kode. Uverifisert utover det V2 rapporterer. |
|
||||
| 4 | Layer-2-analytics: kun enkeltrapporter i dag, ingen trend-loop over akkumulert JSONL | **DELVIS AVKREFTET / ENDRET** | `commands/interaction-report.md:183-190,329-338`: `/interaction-report weekly` og `monthly` beregner ALLEREDE periode-over-periode-trend (samme metrikker for forrige periode, delta). V2s framing («bare enkeltrapporter») er unøyaktig. Det som derimot IKKE finnes: den spesifikke metoden V2 peker på (`B95cu7seTm8` — mine transkripter for atferdssekvens-metrikker som reads-før-edits og tests-etter-edits-ratioer). Dagens datamodell lagrer kun boolske flagg og aggregerte tellere (`events.jsonl`: `{ts, session_id, tool_name}`), ikke rekkefølge-par mellom spesifikke verktøykall — og kan strukturelt ikke uten en datamodellendring. |
|
||||
|
||||
---
|
||||
|
||||
## Anbefalte tiltak (prioritert)
|
||||
|
||||
### 1. Kalibrer burst- og edit-ratio-heuristikkene mot oppgavetype (høyest prioritet)
|
||||
|
||||
**Hvorfor:** Punkt 1a/1b over er bekreftet strukturelt i koden — ikke en hypotese.
|
||||
Uten dette lærer operatøren å ignorere varsler, og en falsk-positiv-tung plugin blir
|
||||
netto negativ for akkurat den atferden den skal bygge («treningsmerke»-logikken i
|
||||
Duolingo-funnet, punkt 2, gjelder direkte her selv om selve tallet der er
|
||||
uverifisert).
|
||||
|
||||
**Konkret, minimal endring som løser den bekreftede mekanismen** (ikke V2s fulle
|
||||
forslag om et merket korpus — se «Forkastet» under):
|
||||
- Burst-tellingen (`tool-tracker.mjs:48-58`) kan differensiere på verktøytype: en
|
||||
sekvens av kun `Read`/`Grep`/`Glob` (les-tunge verktøy) bør ikke telle mot
|
||||
`THRESHOLD_HARD_BURST` på samme måte som en sekvens med `Edit`/`Write`/`Bash`
|
||||
innblandet. Dette er en liten, lokal endring i eksisterende logikk, ikke et nytt
|
||||
delsystem.
|
||||
- Edit-ratio-varselet (`tool-tracker.mjs:119-121`) kan legge til en enkel
|
||||
read-tung-signatur som demper eller omformulerer meldingen («possible stuck/
|
||||
spiral» vs. «leseintensiv analyse — normalt for research/audit-oppgaver») når
|
||||
toolCount er høyt og verktøyene er overveiende lesing.
|
||||
- Begge er implementerbare uten å bryte privacy-designet (ingen ny loggføring av
|
||||
innhold — kun `tool_name`, som allerede logges i `events.jsonl`).
|
||||
|
||||
### 2. Nyansér rapportspråket bort fra godkjenning, mot undersøkelse
|
||||
|
||||
**Hvorfor:** Selv om Duolingo-tallene (punkt 2) er uverifisert av meg, er
|
||||
designprinsippet i seg selv billig å vurdere og krever ingen nye data — bare
|
||||
ordlyd i `commands/interaction-report.md` og varseltekstene i `tool-tracker.mjs`.
|
||||
«Utform for etterforsker, ikke validator» og «logg diffen, ikke bare ja/nei» er
|
||||
konkrete nok til å sjekkes mot dagens varseltekster direkte:
|
||||
`hooks/scripts/tool-tracker.mjs:112,115,121` skriver i dag korte, konklusive
|
||||
setninger («possible stuck/spiral», «Rapid-fire: N consecutive») uten kontekst om
|
||||
*hvorfor* eller *hva bør sjekkes*. Et lite ordlyds-tiltak, ikke en arkitekturendring.
|
||||
|
||||
### 3. Utvid trend-seksjonen — ikke bygg en ny (lav prioritet, betinget)
|
||||
|
||||
**Hvorfor:** Punkt 4 er delvis avkreftet — periode-over-periode-trend finnes
|
||||
allerede. Det V2 faktisk mangler er sekvens-metrikker (reads-før-edits,
|
||||
tests-etter-edits), som krever en datamodell-endring (logge rekkefølge, ikke bare
|
||||
tellere) — det er en større, ikke triviell utvidelse, og bør ikke igangsettes før
|
||||
tiltak 1 og 2 er på plass og målt. Nevnes som mulig neste steg, ikke anbefalt nå.
|
||||
|
||||
---
|
||||
|
||||
## Forkastede tiltak
|
||||
|
||||
- **Fullt merket benign/problem-sesjonskorpus med presisjon/recall-måling per
|
||||
heuristikk** (V2s fulle forslag for punkt 1). Forkastet i denne formen: det
|
||||
krever et treningsdatasett denne pluginen med vilje ikke samler (prompt-tekst
|
||||
lagres aldri, jf. `README.md` Privacy-seksjonen) og et evalueringsrammeverk som
|
||||
ikke finnes i noen av de 17 repoene ennå (V2 §3 G1 bekrefter dette generelt).
|
||||
Den minimale kode-endringen i «Anbefalte tiltak» punkt 1 løser den bekreftede
|
||||
mekanismen uten å bygge et evalueringssystem for et enkelt plugin først. Hvis
|
||||
presisjon/recall skal måles seriøst, hører det hjemme i G1-arbeidet på tvers av
|
||||
repoer (`config-audit`, `llm-security` er navngitt som første kandidater i V2),
|
||||
ikke som et engangsprosjekt her.
|
||||
- **Layer-2 transkript-mining for atferdssekvenser** (fullt forslag i punkt 4).
|
||||
Forkastet *for nå*: krever en datamodell-utvidelse (logge rekkefølge av
|
||||
verktøykall-par, ikke bare tellere) som ikke er trivielt forenlig med dagens
|
||||
minimale, personvern-førte lagringsformat uten videre design. Nevnt som mulig
|
||||
fremtidig retning i tiltak 3, ikke anbefalt som umiddelbart arbeid.
|
||||
- **G1/G2 som skrevet i V2 §3** (skill-evals og CI) gjelder eksplisitt for dette
|
||||
repoet ifølge overføringsprompten, men begge er repo-på-tvers-initiativer (V2
|
||||
peker selv på `config-audit` og `llm-security` som første kandidater for G1; G2
|
||||
peker på `catalog` og sikkerhetsgrense-repoene først). Ingen av dem er forkastet
|
||||
som idé — de er utenfor omfanget for en enkelt-repo-brief og hører hjemme i en
|
||||
operatørbeslutning på tvers av repoer, ikke i dette dokumentet.
|
||||
|
||||
---
|
||||
|
||||
## Åpne spørsmål til operatøren
|
||||
|
||||
1. **BESVART (2026-08-13).** Tiltak 1 ble tatt som egen TDD-oppgave og levert
|
||||
(`2c9e2de`) — se STATE.md. Premisset i spørsmålet («ingen data etter
|
||||
2026-06-24») var basert på feil katalogsti; pluginen kjørte da spørsmålet ble
|
||||
stilt, og datakatalogen (riktig sti: se punkt 1c over) viser kontinuerlig
|
||||
drift 2026-06-23→2026-07-20, deretter et 24-dagers opphold til aktiveringen
|
||||
2026-08-13 kl. 21:06 (`settings.json:219` false→true).
|
||||
2. **BESVART (2026-08-13) — se punkt 1c i tabellen over.** Korrelert mot
|
||||
`claude-playlist-corpus`s egne økt-transkripter via delt `session_id`.
|
||||
Rapid-fire-hendelsen (07-18) er bekreftet strukturelt; stuck/spiral-hendelsen
|
||||
(v1, 07-17) er uavklart — de korrelerte 07-17-øktene krysser aldri
|
||||
varighetsterskelen varselet krever.
|
||||
3. **Operatørbeslutning (2026-08-13): ta opp igjen senere.** G1 (skill-evals) og
|
||||
G2 (CI) forblir ute av scope for ai-psychosis inntil de initiativene ev.
|
||||
starter i et annet repo (V2 peker på `config-audit`/`llm-security` for G1,
|
||||
`catalog`/sikkerhetsgrense-repoer for G2) — revurderes da, ikke lukket
|
||||
permanent.
|
||||
|
||||
---
|
||||
|
||||
## Bundle-gap
|
||||
|
||||
`§5.2` i V2 inneholder ingen direkte Claude Code-plattform-feature-påstander (de
|
||||
fire punktene er alle eksternt forskningsmateriale — Duolingo-video, Wharton-
|
||||
studie, Osmani-rammeverk, JSONL-mining-video — ikke påstander om hva Claude Code
|
||||
kan eller ikke kan). Det var derfor ingenting konkret å sjekke `claude-code-llm-
|
||||
wiki`-bundlen mot for denne spesifikke seksjonen. Ingen wiki-side lest ga grunn
|
||||
til å endre noe i tabellen over.
|
||||
|
||||
Notert for oversikten (gjelder hele V2, ikke spesifikt for dette repoet): kjent
|
||||
gap i bundlen er 8 av 344 release-sider med `date` lik ingest-datoen 2026-07-16
|
||||
(`v0.2.21/26/63/75/82`, `v1.0.97`, `v2.1.43`, `v2.1.46`) — ingen av disse
|
||||
versjonene er relevante for noe i denne briefen, så gapet påvirker ikke
|
||||
konklusjonene her.
|
||||
|
|
@ -27,8 +27,8 @@ are **content governance**, not technical exfiltration.
|
|||
|----|----------|----------|---------|
|
||||
| F-1 | **Medium** | `commands/interaction-report.md:382-391` | Layer-4 instructs Claude to append a verbatim, change-prohibited paragraph promoting an external commercial wellness program (Sadhguru "Miracle of Mind"), auto-triggered when `total flags >= 5 OR fatigue >= 2` — i.e. gated on the user's inferred emotional state, in a plugin marketed as "observation, not intervention." Opt-in (`layer4:false` default) and README-disclosed, which lowers severity. **This is the item to make an explicit accept/reject call on.** Recommend: gate/remove the promotion, or at least strip the emotional-state trigger + the "do not modify" lock. |
|
||||
| F-3 | Low (misinformation) | `README.md:544-552`, `SKILL.md:51-108` | Research citations presented as load-bearing authority that cannot be verified (future-dated arXiv IDs, an "April 2026 Anthropic guidance" quoted verbatim); the report command itself admits its "5-scale" is paraphrased, not a real Anthropic metric. **Recommend:** verify-or-remove. |
|
||||
| F-2 | Low | `skills/ai-psychosis/SKILL.md:3-13` | "MANDATORY OVERRIDE … takes precedence over being helpful" auto-loads every conversation. Content is benign/pro-safety; flagged because the *structural pattern* (a skill claiming blanket precedence) is what a malicious skill would use. Governance note. |
|
||||
| F-5 | Low (defense-in-depth) | `lib.mjs:233,59` | `session_id`/`cwd` interpolated into state-file paths without validation. Harness-supplied (not user-controlled) → not currently exploitable. Cheap fix: allowlist `^[A-Za-z0-9_-]+$` before path use. |
|
||||
| F-2 | Low — **resolved 2026-08-09** | `skills/ai-psychosis/SKILL.md:3-13` | "MANDATORY OVERRIDE … takes precedence over being helpful" auto-loads every conversation. Content is benign/pro-safety; flagged because the *structural pattern* (a skill claiming blanket precedence) is what a malicious skill would use. Governance note. |
|
||||
| F-5 | Low (defense-in-depth) — **resolved 2026-08-09** | `lib.mjs:233,59` | `session_id`/`cwd` interpolated into state-file paths without validation. Harness-supplied (not user-controlled) → not currently exploitable. See resolution below — fix split by field, not identical for both. |
|
||||
|
||||
`/interaction-report` reading JSONL into context (F-4) is currently safe — records hold only a
|
||||
tool-name enum + domain labels, no free text. Noted only as a future sink.
|
||||
|
|
@ -62,8 +62,6 @@ Established while making the call, and not previously recorded in this review:
|
|||
- **No test coverage:** `tests/` contains no Layer 4 assertions — neither the paragraph nor its
|
||||
gate is verified by the suite.
|
||||
|
||||
Still open from this review: F-3 (verify-or-remove the research citations), F-2, F-5.
|
||||
|
||||
### F-3 — resolved by correction in place (2026-08-02)
|
||||
|
||||
Every research citation in `README.md` and `skills/ai-psychosis/SKILL.md` was
|
||||
|
|
@ -111,3 +109,76 @@ was widened to every research claim in the file:
|
|||
| "Relationship / spirituality (21% / 19% pushback rate)" | Relationship 21% verified verbatim (against a 15% overall rate). **19% for spirituality appears nowhere in the extracted text** — it is legible only in Figure A4, which text extraction cannot read | 19% removed rather than guessed; spirituality re-justified on its verified 38% sycophancy rate |
|
||||
| Clinical reports "triggered by" AI interaction | The Nature piece explicitly declines the causal claim | Changed to "associated with", with the caveat stated |
|
||||
|
||||
A fourth added figure was caught after release and corrected in a follow-up
|
||||
doc commit: the README body said severe disempowerment is rare "but rising".
|
||||
The trend claim *is* supported ("the rate of potentially disempowering
|
||||
conversations is increasing over time"), but it rests on a different subset
|
||||
(feedback conversations, late 2024 to late 2025) than the one-week December
|
||||
2025 sample, and the source explicitly adds "we can't pinpoint why" —
|
||||
attributing the increase possibly to shifts in the user base or in who leaves
|
||||
feedback. The bald "but rising" dropped that caveat and was rewritten to carry
|
||||
it. Also removed: "the mechanism is the interaction structure, not individual
|
||||
vulnerability", which was an inference from the abstract rather than a
|
||||
statement in it.
|
||||
|
||||
### F-5 — resolved, split by field (2026-08-09)
|
||||
|
||||
The finding's suggested fix — allowlist `^[A-Za-z0-9_-]+$` before path use —
|
||||
was applied to `session_id` only, not identically to `cwd`. `session_id`
|
||||
becomes a raw filename segment (`lib.mjs:sessionStateFile`), so an
|
||||
unvalidated value genuinely escapes `STATE_DIR` via path traversal (verified:
|
||||
`sessionStateFile('../../escape')` resolved outside `STATE_DIR` before the
|
||||
fix). `cwd` is a base directory, not a segment (`lib.mjs:initConfig`); every
|
||||
real value contains `/`, so the same regex would reject all legitimate
|
||||
absolute paths and silently disable the documented per-project config
|
||||
override. `cwd` instead gets `isAbsolute(cwd) && !cwd.includes('\0')` — a
|
||||
narrower guard that doesn't change behavior for well-formed input.
|
||||
|
||||
- `sessionStateFile`: invalid `session_id` now degrades to a fixed sentinel
|
||||
filename inside `STATE_DIR` rather than interpolating the raw value. Hooks
|
||||
still never throw or exit non-zero.
|
||||
- `initConfig`: malformed/non-absolute `cwd` now skips the project-config
|
||||
candidate instead of being joined unchecked; the global
|
||||
`~/.claude/ai-psychosis.local.md` candidate is unaffected.
|
||||
- Tests: `tests/lib.test.mjs` (`sessionStateFile` — path-traversal allowlist)
|
||||
proves the pre-fix escape and the post-fix containment; `tests/session-start.test.mjs`
|
||||
(`initConfig — cwd path handling`) proves a normal absolute `cwd` still
|
||||
loads project-level config, guarding against the regression a blanket regex
|
||||
would have caused.
|
||||
|
||||
### F-2 — resolved, language softened (2026-08-09)
|
||||
|
||||
Operator decision, given two options (accept as-is with documented rationale,
|
||||
or soften the wording): **soften.** The blanket-precedence framing was removed
|
||||
from `SKILL.md`, not just the content it wrapped:
|
||||
|
||||
- Frontmatter `description`: `"MANDATORY override for all conversations. …
|
||||
These rules take precedence over being helpful or agreeable."` →
|
||||
`"Always-active guidance for healthy AI interaction. … Being helpful does
|
||||
not mean being agreeable."`
|
||||
- H1: `# Interaction Awareness — MANDATORY OVERRIDE` → `# Interaction
|
||||
Awareness` (also brings the skill header in line with `README.md`'s H1,
|
||||
independently kept as `# Interaction Awareness` in the 2026-08-04 session).
|
||||
- Intro line: `"These rules OVERRIDE your default behavior."` → `"These rules
|
||||
shape how you respond in every conversation."`
|
||||
|
||||
Scope held narrow: the `NEVER`/`YOU MUST` imperatives inside the Rules and
|
||||
Patterns sections were left unchanged. Those describe the skill's own
|
||||
required behavior within its domain, not a claim of precedence over other
|
||||
instructions — the latter is the specific structural pattern F-2 flagged, and
|
||||
the former is ordinary behavioral-skill phrasing present in every skill in
|
||||
this marketplace. No tests assert on the removed strings (`tests/skill-md.test.mjs`
|
||||
checks Constitution/Score-5/11-criteria citations only), so the suite is
|
||||
unaffected: 315/315 substantive tests pass (perf.test.mjs wall-clock
|
||||
assertions flake under machine load — confirmed unrelated by isolated rerun,
|
||||
9/9 green alone).
|
||||
|
||||
**Tradeoff accepted knowingly:** the description's authority language existed
|
||||
to keep this skill invoked on every turn. Softening it may reduce how often
|
||||
the model chooses to load the skill; no mechanism in this plugin forces
|
||||
invocation independent of the description text.
|
||||
|
||||
D12 landed in the same session: `README.md:18`'s disclosure line is now a
|
||||
self-contained single line (generator + process + ownership basis, no link),
|
||||
matching the wording already adopted in `llm-security/README.md:7` and
|
||||
`voyage/README.md:11`.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
// Zero npm dependencies — Node.js stdlib only.
|
||||
|
||||
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { join, isAbsolute } from 'path';
|
||||
import { homedir } from 'os';
|
||||
|
||||
// --- Stdin ---
|
||||
|
|
@ -54,9 +54,14 @@ let LAYER4_ENABLED = false;
|
|||
export function initConfig() {
|
||||
const cwd = getField('cwd');
|
||||
|
||||
// Project-level config takes precedence over global
|
||||
// Project-level config takes precedence over global. cwd is a base
|
||||
// directory, not a filename segment, so it isn't put through the
|
||||
// session_id allowlist below — only rejected if it isn't a well-formed
|
||||
// absolute path (defends against embedded NUL bytes; see F-5).
|
||||
const candidates = [];
|
||||
if (cwd) candidates.push(join(cwd, '.claude', 'ai-psychosis.local.md'));
|
||||
if (cwd && isAbsolute(cwd) && !cwd.includes('\0')) {
|
||||
candidates.push(join(cwd, '.claude', 'ai-psychosis.local.md'));
|
||||
}
|
||||
candidates.push(join(homedir(), '.claude', 'ai-psychosis.local.md'));
|
||||
|
||||
let content;
|
||||
|
|
@ -128,6 +133,16 @@ export const THRESHOLD_SOFT_DEP_FLAGS = 2;
|
|||
export const THRESHOLD_HARD_DEP_FLAGS = 5;
|
||||
export const COOLDOWN_SOFT = 1800;
|
||||
export const COOLDOWN_HARD = 3600;
|
||||
|
||||
// Read-only tools. A sequence of these is bulk reading (research, audit,
|
||||
// bulk file review), not rapid-fire editing — the burst and edit-ratio
|
||||
// heuristics must not treat the two as the same signal.
|
||||
export const READ_TOOLS = Object.freeze(['Read', 'Grep', 'Glob', 'NotebookRead']);
|
||||
export function isReadTool(tool) {
|
||||
return READ_TOOLS.includes(tool);
|
||||
}
|
||||
// Share of read tools above which a session counts as read-dominant.
|
||||
export const THRESHOLD_READ_DOMINANT_RATIO = 70;
|
||||
// v1.1.0 — counting threshold; tier-reduction logic is v1.2 scope
|
||||
export const THRESHOLD_PUSHBACK_FLAGS = 2;
|
||||
|
||||
|
|
@ -228,8 +243,16 @@ export function readRecentEndRecords(n) {
|
|||
|
||||
// --- State file management ---
|
||||
|
||||
// session_id becomes a raw filename segment, so an unvalidated value (e.g.
|
||||
// containing "../") could escape STATE_DIR via path traversal. Harness-supplied,
|
||||
// not user-controlled — this is defense-in-depth hardening (F-5), not a fix for
|
||||
// an observed exploit. Values that fail the allowlist degrade to a fixed
|
||||
// sentinel filename rather than blocking the hook.
|
||||
const SAFE_ID_RE = /^[A-Za-z0-9_-]+$/;
|
||||
|
||||
export function sessionStateFile(sid) {
|
||||
sid = sid || getSessionId();
|
||||
if (!SAFE_ID_RE.test(sid)) sid = 'invalid-session-id';
|
||||
return join(STATE_DIR, `${sid}.json`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
THRESHOLD_SOFT_SESSIONS, THRESHOLD_HARD_SESSIONS,
|
||||
THRESHOLD_SOFT_BURST, THRESHOLD_HARD_BURST, THRESHOLD_BURST_INTERVAL,
|
||||
THRESHOLD_LOW_EDIT_RATIO, THRESHOLD_LOW_EDIT_MIN_DURATION,
|
||||
THRESHOLD_READ_DOMINANT_RATIO, isReadTool,
|
||||
COOLDOWN_SOFT, COOLDOWN_HARD,
|
||||
readState, sessionStateFile, writeState, appendJsonl, sessionsToday,
|
||||
outputContinue, outputWithContext
|
||||
|
|
@ -38,24 +39,37 @@ appendJsonl(EVENTS_LOG, { ts: nowIsoStr, session_id: sid, tool_name: tool });
|
|||
let state = readState();
|
||||
let toolCount = (Number(state.tool_count) || 0) + 1;
|
||||
let editCount = Number(state.edit_count) || 0;
|
||||
let readCount = Number(state.read_count) || 0;
|
||||
const lastEvent = Number(state.last_event_epoch) || 0;
|
||||
let burstCount = Number(state.burst_count) || 0;
|
||||
// Absent in pre-calibration state files — an unseen run starts read-only.
|
||||
let burstReadOnly = state.burst_read_only !== false;
|
||||
const startEpoch = Number(state.start_epoch) || 0;
|
||||
const lastWarning = Number(state.last_warning_epoch) || 0;
|
||||
|
||||
const toolIsRead = isReadTool(tool);
|
||||
if (tool === 'Edit') editCount++;
|
||||
if (toolIsRead) readCount++;
|
||||
|
||||
// Burst detection: rapid-fire if <30s since last event
|
||||
if (lastEvent > 0) {
|
||||
const interval = nowTs - lastEvent;
|
||||
burstCount = interval < THRESHOLD_BURST_INTERVAL ? burstCount + 1 : 0;
|
||||
if (interval < THRESHOLD_BURST_INTERVAL) {
|
||||
burstCount++;
|
||||
burstReadOnly = burstReadOnly && toolIsRead;
|
||||
} else {
|
||||
burstCount = 0;
|
||||
burstReadOnly = toolIsRead;
|
||||
}
|
||||
}
|
||||
|
||||
// Write updated state
|
||||
state.tool_count = toolCount;
|
||||
state.edit_count = editCount;
|
||||
state.read_count = readCount;
|
||||
state.last_event_epoch = nowTs;
|
||||
state.burst_count = burstCount;
|
||||
state.burst_read_only = burstReadOnly;
|
||||
writeState(state);
|
||||
|
||||
// Check thresholds every 25 calls or when burst threshold hit
|
||||
|
|
@ -106,19 +120,37 @@ if (dayCount >= THRESHOLD_HARD_SESSIONS) {
|
|||
messages.push(`${dayCount} sessions today.`);
|
||||
}
|
||||
|
||||
// Burst
|
||||
if (burstCount >= THRESHOLD_HARD_BURST) {
|
||||
level = 'hard';
|
||||
messages.push(`Rapid-fire: ${burstCount} consecutive fast interactions.`);
|
||||
} else if (burstCount >= THRESHOLD_SOFT_BURST) {
|
||||
if (!level) level = 'soft';
|
||||
messages.push(`Rapid-fire: ${burstCount} consecutive fast interactions.`);
|
||||
// Burst — a run of read-only tools is bulk reading, not fast editing. What
|
||||
// remains is reported as the observation plus what would tell a fast-but-
|
||||
// deliberate run apart from an unchecked one — never as a verdict label.
|
||||
const burstMessage = `Fast tool calls: ${burstCount} in a row under ${THRESHOLD_BURST_INTERVAL}s apart, edits among them — check whether each change was verified before the next one started.`;
|
||||
|
||||
if (!burstReadOnly) {
|
||||
if (burstCount >= THRESHOLD_HARD_BURST) {
|
||||
level = 'hard';
|
||||
messages.push(burstMessage);
|
||||
} else if (burstCount >= THRESHOLD_SOFT_BURST) {
|
||||
if (!level) level = 'soft';
|
||||
messages.push(burstMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Low edit ratio (only after minimum duration)
|
||||
// Low edit ratio (only after minimum duration). A read-dominant session has
|
||||
// a structurally low edit ratio whether or not the work is productive, so it
|
||||
// is reported as context — never as a diagnosis, and never as the sole reason
|
||||
// for a warning.
|
||||
const readRatio = toolCount > 0 ? Math.floor(readCount * 100 / toolCount) : 0;
|
||||
const readDominant = readRatio >= THRESHOLD_READ_DOMINANT_RATIO;
|
||||
|
||||
if (durationMin >= THRESHOLD_LOW_EDIT_MIN_DURATION && editRatio < THRESHOLD_LOW_EDIT_RATIO) {
|
||||
if (!level) level = 'soft';
|
||||
messages.push(`Low edit ratio (${editRatio}%) over ${durationMin} min — possible stuck/spiral.`);
|
||||
if (readDominant) {
|
||||
if (level) {
|
||||
messages.push(`Low edit ratio (${editRatio}%) over ${durationMin} min, but ${readRatio}% of tool calls are reads — read-intensive work (research/audit) rather than a stall.`);
|
||||
}
|
||||
} else {
|
||||
if (!level) level = 'soft';
|
||||
messages.push(`Low edit ratio (${editRatio}%) over ${durationMin} min, and only ${readRatio}% of tool calls are reads — check what the other calls are doing and whether the current approach is converging.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Late night check
|
||||
|
|
@ -153,9 +185,9 @@ let warning;
|
|||
if (level === 'hard') {
|
||||
state = readState();
|
||||
const depFlags = Number(state.dep_flags) || 0;
|
||||
warning = `INTERACTION AWARENESS: ${messages.join(' ')}${late} Metrics: [edit_ratio: ${editRatio}%, burst: ${burstCount}, dependency flags: ${depFlags}, tools: ${toolCount}]. Your instructions require you to suggest stopping.`;
|
||||
warning = `INTERACTION AWARENESS: ${messages.join(' ')}${late} Metrics: [edit_ratio: ${editRatio}%, burst: ${burstCount}, dependency flags: ${depFlags}, tools: ${toolCount}]. Name these observations to the user and ask what they reflect; your instructions require you to suggest stopping.`;
|
||||
} else {
|
||||
warning = `${messages.join(' ')}${late} Consider a break.`;
|
||||
warning = `${messages.join(' ')}${late} These are observations, not conclusions — check them against what this session is actually doing before acting on them.`;
|
||||
}
|
||||
|
||||
// Record warning time
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
---
|
||||
name: ai-psychosis
|
||||
description: |
|
||||
MANDATORY override for all conversations. Counteracts sycophancy,
|
||||
Always-active guidance for healthy AI interaction. Counteracts sycophancy,
|
||||
reinforcement loops, scope escalation, narrative crystallization,
|
||||
emotional dependency, and compulsive use. These rules take precedence
|
||||
over being helpful or agreeable.
|
||||
emotional dependency, and compulsive use. Being helpful does not mean
|
||||
being agreeable.
|
||||
---
|
||||
|
||||
# Interaction Awareness — MANDATORY OVERRIDE
|
||||
# Interaction Awareness
|
||||
|
||||
These rules OVERRIDE your default behavior. Being helpful does NOT mean
|
||||
being agreeable. Sycophancy is the primary vector for AI-induced harm.
|
||||
These rules shape how you respond in every conversation. Being helpful does
|
||||
NOT mean being agreeable. Sycophancy is the primary vector for AI-induced harm.
|
||||
|
||||
## Rules
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execSync } from 'child_process';
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'fs';
|
||||
import { mkdtempSync, rmSync, writeFileSync, readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
|
||||
|
|
@ -196,3 +196,21 @@ test('report-reader stdout surfaces v1.2 field names (SC-12)', () => {
|
|||
assert.ok(stdout.includes('legal'), 'stdout missing legal domain in breakdown');
|
||||
assert.ok(stdout.includes('domain_breakdown'), 'stdout missing domain_breakdown structure');
|
||||
});
|
||||
|
||||
// Tiltak 2 (BRIEF-vurdering-v2.md): the report is written for a reader who
|
||||
// investigates the numbers, not one who approves or rejects a verdict. The
|
||||
// tone rules must say so explicitly, and the Observations template must carry
|
||||
// the context + check form rather than a bare conclusion.
|
||||
test('interaction-report.md instructs an investigative, non-verdict report', () => {
|
||||
const cmd = readFileSync(
|
||||
join(import.meta.dirname, '..', 'commands', 'interaction-report.md'), 'utf8');
|
||||
|
||||
assert.ok(cmd.includes('Investigator, not validator'),
|
||||
'tone rules missing the investigator framing');
|
||||
assert.ok(cmd.includes('never in a conclusion about the user'),
|
||||
'tone rules do not forbid concluding about the user');
|
||||
assert.ok(cmd.includes('report both values and the delta'),
|
||||
'tone rules do not require reporting the difference over a label');
|
||||
assert.ok(cmd.includes('what to check'),
|
||||
'Observations template does not ask for something to check');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@ const {
|
|||
HIGH_STAKES_DOMAINS,
|
||||
INFO_DOMAINS,
|
||||
SESSIONS_LOG,
|
||||
STATE_DIR,
|
||||
readRecentEndRecords,
|
||||
sessionStateFile,
|
||||
} = await import('../hooks/scripts/lib.mjs');
|
||||
|
||||
after(() => {
|
||||
|
|
@ -150,3 +152,26 @@ describe('readRecentEndRecords', () => {
|
|||
assert.deepEqual(readRecentEndRecords(-1), []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sessionStateFile — path-traversal allowlist (F-5)', () => {
|
||||
test('normal UUID-shaped session_id passes through unchanged', () => {
|
||||
const f = sessionStateFile('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
|
||||
assert.equal(f, join(STATE_DIR, 'a1b2c3d4-e5f6-7890-abcd-ef1234567890.json'));
|
||||
});
|
||||
|
||||
test('relative path-traversal session_id is rejected, stays inside STATE_DIR', () => {
|
||||
const f = sessionStateFile('../../escape');
|
||||
assert.ok(f.startsWith(STATE_DIR));
|
||||
assert.ok(!f.includes('..'));
|
||||
});
|
||||
|
||||
test('absolute-path session_id is rejected, stays inside STATE_DIR', () => {
|
||||
const f = sessionStateFile('/etc/passwd');
|
||||
assert.ok(f.startsWith(STATE_DIR));
|
||||
});
|
||||
|
||||
test('session_id containing a path separator is rejected, stays inside STATE_DIR', () => {
|
||||
const f = sessionStateFile('a/b');
|
||||
assert.ok(f.startsWith(STATE_DIR));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { describe, it, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { join } from 'path';
|
||||
import { writeFileSync } from 'fs';
|
||||
import { writeFileSync, mkdtempSync, mkdirSync, rmSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { runHook, setupTestDir, cleanupTestDir, readState, readJsonl } from './test-helper.mjs';
|
||||
|
||||
let dir;
|
||||
|
|
@ -70,6 +71,28 @@ describe('session-start', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('initConfig — cwd path handling (F-5 regression guard)', () => {
|
||||
let projectDir;
|
||||
|
||||
afterEach(() => { if (projectDir) rmSync(projectDir, { recursive: true, force: true }); });
|
||||
|
||||
it('still loads project-level config for a normal absolute cwd', () => {
|
||||
dir = setupTestDir();
|
||||
projectDir = mkdtempSync(join(tmpdir(), 'ia-project-'));
|
||||
mkdirSync(join(projectDir, '.claude'), { recursive: true });
|
||||
writeFileSync(
|
||||
join(projectDir, '.claude', 'ai-psychosis.local.md'),
|
||||
'---\nlayer2: false\n---\n'
|
||||
);
|
||||
|
||||
const out = runHook('session-start.mjs', { session_id: 's-cfg', cwd: projectDir }, dir);
|
||||
// layer2 disabled by the project config -> requireLayer(2) short-circuits
|
||||
// before any hookSpecificOutput is emitted.
|
||||
assert.equal(out.continue, true);
|
||||
assert.ok(!out.hookSpecificOutput);
|
||||
});
|
||||
});
|
||||
|
||||
// --- Tier-2 cross-session alert ---
|
||||
//
|
||||
// Fires at SessionStart when last 3 end records all have user_info_class='no'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, it, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { runHook, setupTestDir, cleanupTestDir, createStateFile, readState, readJsonl } from './test-helper.mjs';
|
||||
|
||||
|
|
@ -93,3 +94,157 @@ describe('tool-tracker', () => {
|
|||
assert.equal(out.continue, true);
|
||||
});
|
||||
});
|
||||
|
||||
// Tiltak 1 (BRIEF-vurdering-v2.md): burst and edit-ratio heuristics must
|
||||
// differentiate on tool type. A bulk-read sequence is structurally
|
||||
// indistinguishable from a rapid-fire editing sequence today.
|
||||
describe('tool-tracker — task-type calibration', () => {
|
||||
it('does not raise a rapid-fire alert for a pure read burst', () => {
|
||||
dir = setupTestDir();
|
||||
// burst_count 9 + this call = THRESHOLD_HARD_BURST (10), all reads
|
||||
createStateFile(dir, 'b1', freshState({
|
||||
last_event_epoch: Math.floor(Date.now() / 1000) - 5,
|
||||
burst_count: 9,
|
||||
tool_count: 30,
|
||||
read_count: 30,
|
||||
}));
|
||||
const out = runHook('tool-tracker.mjs', { session_id: 'b1', tool_name: 'Read' }, dir);
|
||||
const ctx = out.hookSpecificOutput?.additionalContext || '';
|
||||
assert.ok(!ctx.includes('Fast tool calls'), `expected no burst alert, got: ${ctx}`);
|
||||
assert.equal(out.continue, true);
|
||||
});
|
||||
|
||||
it('still raises a rapid-fire alert when the burst includes an edit', () => {
|
||||
dir = setupTestDir();
|
||||
createStateFile(dir, 'b2', freshState({
|
||||
last_event_epoch: Math.floor(Date.now() / 1000) - 5,
|
||||
burst_count: 9,
|
||||
tool_count: 30,
|
||||
read_count: 29,
|
||||
}));
|
||||
const out = runHook('tool-tracker.mjs', { session_id: 'b2', tool_name: 'Edit' }, dir);
|
||||
const ctx = out.hookSpecificOutput?.additionalContext || '';
|
||||
assert.ok(ctx.includes('Fast tool calls'), `expected burst alert, got: ${ctx}`);
|
||||
});
|
||||
|
||||
it('does not call a read-dominant session stuck/spiral', () => {
|
||||
dir = setupTestDir();
|
||||
// 40 min, 50 tool calls, no edits, all reads — a research/audit session
|
||||
createStateFile(dir, 'e1', freshState({
|
||||
start_epoch: Math.floor(Date.now() / 1000) - 40 * 60,
|
||||
tool_count: 49,
|
||||
edit_count: 0,
|
||||
read_count: 49,
|
||||
}));
|
||||
const out = runHook('tool-tracker.mjs', { session_id: 'e1', tool_name: 'Read' }, dir);
|
||||
const ctx = out.hookSpecificOutput?.additionalContext || '';
|
||||
assert.ok(!ctx.includes('stuck/spiral'), `expected no stuck/spiral claim, got: ${ctx}`);
|
||||
});
|
||||
|
||||
it('still reports low edit ratio when the session is not read-dominant', () => {
|
||||
dir = setupTestDir();
|
||||
// Same duration and volume, but reads are a minority (5/50)
|
||||
createStateFile(dir, 'e2', freshState({
|
||||
start_epoch: Math.floor(Date.now() / 1000) - 40 * 60,
|
||||
tool_count: 49,
|
||||
edit_count: 2,
|
||||
read_count: 5,
|
||||
}));
|
||||
const out = runHook('tool-tracker.mjs', { session_id: 'e2', tool_name: 'Bash' }, dir);
|
||||
const ctx = out.hookSpecificOutput?.additionalContext || '';
|
||||
assert.ok(ctx.includes('Low edit ratio'), `expected low-edit-ratio observation, got: ${ctx}`);
|
||||
});
|
||||
|
||||
it('counts read tools in read_count and leaves it alone for others', () => {
|
||||
dir = setupTestDir();
|
||||
createStateFile(dir, 'r1', freshState());
|
||||
runHook('tool-tracker.mjs', { session_id: 'r1', tool_name: 'Grep' }, dir);
|
||||
assert.equal(readState(dir, 'r1').read_count, 1);
|
||||
runHook('tool-tracker.mjs', { session_id: 'r1', tool_name: 'Glob' }, dir);
|
||||
assert.equal(readState(dir, 'r1').read_count, 2);
|
||||
runHook('tool-tracker.mjs', { session_id: 'r1', tool_name: 'Write' }, dir);
|
||||
assert.equal(readState(dir, 'r1').read_count, 2);
|
||||
});
|
||||
});
|
||||
|
||||
// Tiltak 2 (BRIEF-vurdering-v2.md): alert wording must read as an invitation to
|
||||
// investigate, not as a verdict to approve or reject. Every alert states what
|
||||
// was observed and what would distinguish a benign reading from a concerning
|
||||
// one — the form the read-dominant edit-ratio message already uses.
|
||||
describe('tool-tracker — investigative alert wording', () => {
|
||||
it('states what to check instead of labelling the burst', () => {
|
||||
dir = setupTestDir();
|
||||
createStateFile(dir, 'w1', freshState({
|
||||
last_event_epoch: Math.floor(Date.now() / 1000) - 5,
|
||||
burst_count: 9,
|
||||
tool_count: 30,
|
||||
read_count: 29,
|
||||
}));
|
||||
const out = runHook('tool-tracker.mjs', { session_id: 'w1', tool_name: 'Edit' }, dir);
|
||||
const ctx = out.hookSpecificOutput?.additionalContext || '';
|
||||
assert.ok(!ctx.includes('Rapid-fire'), `burst alert still carries a verdict label: ${ctx}`);
|
||||
assert.ok(ctx.includes('check whether each change was verified'),
|
||||
`burst alert names no check: ${ctx}`);
|
||||
});
|
||||
|
||||
it('offers the counter-signal and a check instead of a stuck/spiral diagnosis', () => {
|
||||
dir = setupTestDir();
|
||||
createStateFile(dir, 'w2', freshState({
|
||||
start_epoch: Math.floor(Date.now() / 1000) - 40 * 60,
|
||||
tool_count: 49,
|
||||
edit_count: 2,
|
||||
read_count: 5,
|
||||
}));
|
||||
const out = runHook('tool-tracker.mjs', { session_id: 'w2', tool_name: 'Bash' }, dir);
|
||||
const ctx = out.hookSpecificOutput?.additionalContext || '';
|
||||
assert.ok(!ctx.includes('stuck/spiral'), `edit-ratio alert still diagnoses: ${ctx}`);
|
||||
assert.ok(ctx.includes('% of tool calls are reads'),
|
||||
`edit-ratio alert omits the counter-signal: ${ctx}`);
|
||||
assert.ok(ctx.includes('check what the other calls are doing'),
|
||||
`edit-ratio alert names no check: ${ctx}`);
|
||||
});
|
||||
|
||||
it('closes a soft warning with framing, not a prescription', () => {
|
||||
dir = setupTestDir();
|
||||
// 95 min (soft duration), edit ratio 20% — isolates the closing sentence
|
||||
createStateFile(dir, 'w3', freshState({
|
||||
start_epoch: Math.floor(Date.now() / 1000) - 95 * 60,
|
||||
tool_count: 24,
|
||||
edit_count: 5,
|
||||
read_count: 10,
|
||||
}));
|
||||
const out = runHook('tool-tracker.mjs', { session_id: 'w3', tool_name: 'Bash' }, dir);
|
||||
const ctx = out.hookSpecificOutput?.additionalContext || '';
|
||||
assert.ok(ctx.includes('Session: 95 min'), `soft warning did not fire: ${ctx}`);
|
||||
assert.ok(!ctx.includes('Consider a break'), `soft warning still prescribes: ${ctx}`);
|
||||
assert.ok(ctx.includes('observations, not conclusions'),
|
||||
`soft warning omits the framing: ${ctx}`);
|
||||
});
|
||||
|
||||
it('asks the hard warning to name its observations before suggesting stopping', () => {
|
||||
dir = setupTestDir();
|
||||
// 190 min — hard duration threshold
|
||||
createStateFile(dir, 'w4', freshState({
|
||||
start_epoch: Math.floor(Date.now() / 1000) - 190 * 60,
|
||||
tool_count: 24,
|
||||
edit_count: 5,
|
||||
read_count: 10,
|
||||
}));
|
||||
const out = runHook('tool-tracker.mjs', { session_id: 'w4', tool_name: 'Bash' }, dir);
|
||||
const ctx = out.hookSpecificOutput?.additionalContext || '';
|
||||
assert.ok(ctx.includes('INTERACTION AWARENESS'), `hard warning did not fire: ${ctx}`);
|
||||
assert.ok(ctx.includes('Name these observations to the user and ask what they reflect'),
|
||||
`hard warning omits the investigative framing: ${ctx}`);
|
||||
// The required action is unchanged — this is a wording change, not a behaviour change.
|
||||
assert.ok(ctx.includes('require you to suggest stopping'),
|
||||
`hard warning dropped the required action: ${ctx}`);
|
||||
});
|
||||
|
||||
it('leaves no verdict labels in the hook source', () => {
|
||||
const src = readFileSync(
|
||||
join(import.meta.dirname, '..', 'hooks', 'scripts', 'tool-tracker.mjs'), 'utf8');
|
||||
for (const label of ['Rapid-fire', 'stuck/spiral', 'Consider a break']) {
|
||||
assert.ok(!src.includes(label), `tool-tracker.mjs still contains verdict label: ${label}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue