Compare commits

..

46 Commits

Author SHA1 Message Date
ecc-tools[bot]
06c8402b19 feat: add everything-claude-code ECC bundle (.claude/commands/add-or-update-skill.md) 2026-03-24 10:44:35 +00:00
ecc-tools[bot]
e9264d84f5 feat: add everything-claude-code ECC bundle (.claude/commands/feature-development.md) 2026-03-24 10:44:34 +00:00
ecc-tools[bot]
9c74492f86 feat: add everything-claude-code ECC bundle (.claude/commands/database-migration.md) 2026-03-24 10:44:34 +00:00
ecc-tools[bot]
f38fff2155 feat: add everything-claude-code ECC bundle (.claude/enterprise/controls.md) 2026-03-24 10:44:33 +00:00
ecc-tools[bot]
080194436d feat: add everything-claude-code ECC bundle (.claude/team/everything-claude-code-team-config.json) 2026-03-24 10:44:32 +00:00
ecc-tools[bot]
6ef930ed3e feat: add everything-claude-code ECC bundle (.claude/research/everything-claude-code-research-playbook.md) 2026-03-24 10:44:31 +00:00
ecc-tools[bot]
a5e40ef0f7 feat: add everything-claude-code ECC bundle (.claude/rules/everything-claude-code-guardrails.md) 2026-03-24 10:44:30 +00:00
ecc-tools[bot]
cffe9cf953 feat: add everything-claude-code ECC bundle (.codex/agents/docs-researcher.toml) 2026-03-24 10:44:29 +00:00
ecc-tools[bot]
09e85c4350 feat: add everything-claude-code ECC bundle (.codex/agents/reviewer.toml) 2026-03-24 10:44:28 +00:00
ecc-tools[bot]
ae19bad71f feat: add everything-claude-code ECC bundle (.codex/agents/explorer.toml) 2026-03-24 10:44:27 +00:00
ecc-tools[bot]
9a24665935 feat: add everything-claude-code ECC bundle (.claude/identity.json) 2026-03-24 10:44:26 +00:00
ecc-tools[bot]
c1327505ad feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/agents/openai.yaml) 2026-03-24 10:44:25 +00:00
ecc-tools[bot]
1571d23a40 feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/SKILL.md) 2026-03-24 10:44:25 +00:00
ecc-tools[bot]
c8c6ebabb3 feat: add everything-claude-code ECC bundle (.claude/skills/everything-claude-code/SKILL.md) 2026-03-24 10:44:24 +00:00
ecc-tools[bot]
db4ffbc398 feat: add everything-claude-code ECC bundle (.claude/ecc-tools.json) 2026-03-24 10:44:23 +00:00
ecc-tools[bot]
f2de3453a7 feat: add everything-claude-code ECC bundle (.claude/commands/add-or-update-skill.md) 2026-03-24 10:43:47 +00:00
ecc-tools[bot]
aa98844de4 feat: add everything-claude-code ECC bundle (.claude/commands/feature-development.md) 2026-03-24 10:43:46 +00:00
ecc-tools[bot]
bad5ebf7b0 feat: add everything-claude-code ECC bundle (.claude/commands/database-migration.md) 2026-03-24 10:43:45 +00:00
ecc-tools[bot]
62e4d16008 feat: add everything-claude-code ECC bundle (.claude/enterprise/controls.md) 2026-03-24 10:43:44 +00:00
ecc-tools[bot]
4a4c01b7ec feat: add everything-claude-code ECC bundle (.claude/team/everything-claude-code-team-config.json) 2026-03-24 10:43:43 +00:00
ecc-tools[bot]
e3d1500efb feat: add everything-claude-code ECC bundle (.claude/research/everything-claude-code-research-playbook.md) 2026-03-24 10:43:43 +00:00
ecc-tools[bot]
7eaae99cc7 feat: add everything-claude-code ECC bundle (.claude/rules/everything-claude-code-guardrails.md) 2026-03-24 10:43:42 +00:00
ecc-tools[bot]
b7012da28b feat: add everything-claude-code ECC bundle (.codex/agents/docs-researcher.toml) 2026-03-24 10:43:41 +00:00
ecc-tools[bot]
4be983ac03 feat: add everything-claude-code ECC bundle (.codex/agents/reviewer.toml) 2026-03-24 10:43:40 +00:00
ecc-tools[bot]
350c43e10a feat: add everything-claude-code ECC bundle (.codex/agents/explorer.toml) 2026-03-24 10:43:39 +00:00
ecc-tools[bot]
10177640ff feat: add everything-claude-code ECC bundle (.claude/identity.json) 2026-03-24 10:43:38 +00:00
ecc-tools[bot]
72f8ec41f3 feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/agents/openai.yaml) 2026-03-24 10:43:37 +00:00
ecc-tools[bot]
49788ec1f3 feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/SKILL.md) 2026-03-24 10:43:36 +00:00
ecc-tools[bot]
203ce6a7d8 feat: add everything-claude-code ECC bundle (.claude/skills/everything-claude-code/SKILL.md) 2026-03-24 10:43:35 +00:00
ecc-tools[bot]
280b7ef537 feat: add everything-claude-code ECC bundle (.claude/ecc-tools.json) 2026-03-24 10:43:34 +00:00
ecc-tools[bot]
22bb2ff0e7 feat: add everything-claude-code ECC bundle (.claude/commands/add-or-update-skill-documentation.md) 2026-03-24 10:42:54 +00:00
ecc-tools[bot]
0f3c64d7f9 feat: add everything-claude-code ECC bundle (.claude/commands/feature-development.md) 2026-03-24 10:42:53 +00:00
ecc-tools[bot]
b5017c2a70 feat: add everything-claude-code ECC bundle (.claude/commands/database-migration.md) 2026-03-24 10:42:52 +00:00
ecc-tools[bot]
e416d9459f feat: add everything-claude-code ECC bundle (.claude/enterprise/controls.md) 2026-03-24 10:42:51 +00:00
ecc-tools[bot]
7e16420feb feat: add everything-claude-code ECC bundle (.claude/team/everything-claude-code-team-config.json) 2026-03-24 10:42:51 +00:00
ecc-tools[bot]
81ad3abb82 feat: add everything-claude-code ECC bundle (.claude/research/everything-claude-code-research-playbook.md) 2026-03-24 10:42:50 +00:00
ecc-tools[bot]
830e591fba feat: add everything-claude-code ECC bundle (.claude/rules/everything-claude-code-guardrails.md) 2026-03-24 10:42:49 +00:00
ecc-tools[bot]
8a76b4a93f feat: add everything-claude-code ECC bundle (.codex/agents/docs-researcher.toml) 2026-03-24 10:42:48 +00:00
ecc-tools[bot]
bed40d72a9 feat: add everything-claude-code ECC bundle (.codex/agents/reviewer.toml) 2026-03-24 10:42:47 +00:00
ecc-tools[bot]
c19d101e09 feat: add everything-claude-code ECC bundle (.codex/agents/explorer.toml) 2026-03-24 10:42:46 +00:00
ecc-tools[bot]
e006aacac8 feat: add everything-claude-code ECC bundle (.claude/identity.json) 2026-03-24 10:42:45 +00:00
ecc-tools[bot]
d7802cfba1 feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/agents/openai.yaml) 2026-03-24 10:42:44 +00:00
ecc-tools[bot]
60070e54fb feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/SKILL.md) 2026-03-24 10:42:43 +00:00
ecc-tools[bot]
3782530e32 feat: add everything-claude-code ECC bundle (.claude/skills/everything-claude-code/SKILL.md) 2026-03-24 10:42:43 +00:00
ecc-tools[bot]
23c3284848 feat: add everything-claude-code ECC bundle (.claude/ecc-tools.json) 2026-03-24 10:42:42 +00:00
Affaan Mustafa
72497ccae7 feat(ecc2): add tool risk scoring and actions 2026-03-24 03:39:53 -07:00
22 changed files with 678 additions and 1111 deletions

View File

@@ -5,7 +5,7 @@ description: Development conventions and patterns for everything-claude-code. Ja
# Everything Claude Code Conventions
> Generated from [affaan-m/everything-claude-code](https://github.com/affaan-m/everything-claude-code) on 2026-03-20
> Generated from [affaan-m/everything-claude-code](https://github.com/affaan-m/everything-claude-code) on 2026-03-24
## Overview
@@ -33,14 +33,14 @@ Follow these commit message conventions based on 500 analyzed commits.
### Prefixes Used
- `fix`
- `test`
- `feat`
- `fix`
- `docs`
- `test`
### Message Guidelines
- Average message length: ~65 characters
- Average message length: ~62 characters
- Keep first line concise and descriptive
- Use imperative mood ("Add feature" not "Added feature")
@@ -48,49 +48,49 @@ Follow these commit message conventions based on 500 analyzed commits.
*Commit message example*
```text
feat(rules): add C# language support
feat: add everything-claude-code ECC bundle (.claude/commands/add-or-update-skill.md)
```
*Commit message example*
```text
chore(deps-dev): bump flatted (#675)
perf(hooks): move post-edit-format and post-edit-typecheck to strict-only (#757)
```
*Commit message example*
```text
fix: auto-detect ECC root from plugin cache when CLAUDE_PLUGIN_ROOT is unset (#547) (#691)
fix: safe Codex config sync — merge AGENTS.md + add-only MCP servers (#723)
```
*Commit message example*
```text
docs: add Antigravity setup and usage guide (#552)
docs(zh-CN): translate code block(plain text) (#753)
```
*Commit message example*
```text
merge: PR #529 — feat(skills): add documentation-lookup, bun-runtime, nextjs-turbopack; feat(agents): add rust-reviewer
security: remove supply chain risks, external promotions, and unauthorized credits
```
*Commit message example*
```text
Revert "Add Kiro IDE support (.kiro/) (#548)"
feat: add everything-claude-code ECC bundle (.claude/commands/feature-development.md)
```
*Commit message example*
```text
Add Kiro IDE support (.kiro/) (#548)
feat: add everything-claude-code ECC bundle (.claude/commands/database-migration.md)
```
*Commit message example*
```text
feat: add block-no-verify hook for Claude Code and Cursor (#649)
feat: add everything-claude-code ECC bundle (.claude/enterprise/controls.md)
```
## Architecture
@@ -196,21 +196,20 @@ Database schema changes with migration files
3. Generate/update types
**Files typically involved**:
- `**/schema.*`
- `migrations/*`
**Example commit sequence**:
```
feat: implement --with/--without selective install flags (#679)
fix: sync catalog counts with filesystem (27 agents, 113 skills, 58 commands) (#693)
feat(rules): add Rust language rules (rebased #660) (#686)
Add Turkish (tr) docs and update README (#744)
docs(zh-CN): translate code block(plain text) (#753)
fix(install): add rust, cpp, csharp to legacy language alias map (#747)
```
### Feature Development
Standard feature implementation workflow
**Frequency**: ~22 times per month
**Frequency**: ~26 times per month
**Steps**:
1. Add feature implementation
@@ -219,204 +218,136 @@ Standard feature implementation workflow
**Files typically involved**:
- `manifests/*`
- `schemas/*`
- `**/*.test.*`
- `**/api/**`
**Example commit sequence**:
```
feat(skills): add documentation-lookup, bun-runtime, nextjs-turbopack; feat(agents): add rust-reviewer
docs(skills): align documentation-lookup with CONTRIBUTING template; add cross-harness (Codex/Cursor) skill copies
fix: address PR review — skill template (When to use, How it works, Examples), bun.lock, next build note, rust-reviewer CI note, doc-lookup privacy/uncertainty
Merge pull request #736 from pvgomes/docs/add-brazilian-portuguese-translation
fix: bump plugin.json and marketplace.json to v1.9.0
Add Turkish (tr) docs and update README (#744)
```
### Add Language Rules
### Add Or Update Skill
Adds a new programming language to the rules system, including coding style, hooks, patterns, security, and testing guidelines.
**Frequency**: ~2 times per month
**Steps**:
1. Create a new directory under rules/{language}/
2. Add coding-style.md, hooks.md, patterns.md, security.md, and testing.md files with language-specific content
3. Optionally reference or link to related skills
**Files typically involved**:
- `rules/*/coding-style.md`
- `rules/*/hooks.md`
- `rules/*/patterns.md`
- `rules/*/security.md`
- `rules/*/testing.md`
**Example commit sequence**:
```
Create a new directory under rules/{language}/
Add coding-style.md, hooks.md, patterns.md, security.md, and testing.md files with language-specific content
Optionally reference or link to related skills
```
### Add New Skill
Adds a new skill to the system, documenting its workflow, triggers, and usage, often with supporting scripts.
**Frequency**: ~4 times per month
**Steps**:
1. Create a new directory under skills/{skill-name}/
2. Add SKILL.md with documentation (When to Use, How It Works, Examples, etc.)
3. Optionally add scripts or supporting files under skills/{skill-name}/scripts/
4. Address review feedback and iterate on documentation
**Files typically involved**:
- `skills/*/SKILL.md`
- `skills/*/scripts/*.sh`
- `skills/*/scripts/*.js`
**Example commit sequence**:
```
Create a new directory under skills/{skill-name}/
Add SKILL.md with documentation (When to Use, How It Works, Examples, etc.)
Optionally add scripts or supporting files under skills/{skill-name}/scripts/
Address review feedback and iterate on documentation
```
### Add New Agent
Adds a new agent to the system for code review, build resolution, or other automated tasks.
**Frequency**: ~2 times per month
**Steps**:
1. Create a new agent markdown file under agents/{agent-name}.md
2. Register the agent in AGENTS.md
3. Optionally update README.md and docs/COMMAND-AGENT-MAP.md
**Files typically involved**:
- `agents/*.md`
- `AGENTS.md`
- `README.md`
- `docs/COMMAND-AGENT-MAP.md`
**Example commit sequence**:
```
Create a new agent markdown file under agents/{agent-name}.md
Register the agent in AGENTS.md
Optionally update README.md and docs/COMMAND-AGENT-MAP.md
```
### Add New Command
Adds a new command to the system, often paired with a backing skill.
**Frequency**: ~1 times per month
**Steps**:
1. Create a new markdown file under commands/{command-name}.md
2. Optionally add or update a backing skill under skills/{skill-name}/SKILL.md
**Files typically involved**:
- `commands/*.md`
- `skills/*/SKILL.md`
**Example commit sequence**:
```
Create a new markdown file under commands/{command-name}.md
Optionally add or update a backing skill under skills/{skill-name}/SKILL.md
```
### Sync Catalog Counts
Synchronizes the documented counts of agents, skills, and commands in AGENTS.md and README.md with the actual repository state.
Adds or updates a skill in the ECC system, including documentation and provenance.
**Frequency**: ~3 times per month
**Steps**:
1. Update agent, skill, and command counts in AGENTS.md
2. Update the same counts in README.md (quick-start, comparison table, etc.)
3. Optionally update other documentation files
**Files typically involved**:
- `AGENTS.md`
- `README.md`
**Example commit sequence**:
```
Update agent, skill, and command counts in AGENTS.md
Update the same counts in README.md (quick-start, comparison table, etc.)
Optionally update other documentation files
```
### Add Cross Harness Skill Copies
Adds skill copies for different agent harnesses (e.g., Codex, Cursor, Antigravity) to ensure compatibility across platforms.
**Frequency**: ~2 times per month
**Steps**:
1. Copy or adapt SKILL.md to .agents/skills/{skill}/SKILL.md and/or .cursor/skills/{skill}/SKILL.md
2. Optionally add harness-specific openai.yaml or config files
3. Address review feedback to align with CONTRIBUTING template
1. Create or update SKILL.md in the appropriate skills directory (e.g., skills/{skill-name}/SKILL.md or .claude/skills/{skill-name}/SKILL.md)
2. Optionally add or update agent YAML files (e.g., .agents/skills/{skill-name}/agents/*.yaml)
3. Optionally update provenance or placement policy files (e.g., schemas/provenance.schema.json, docs/SKILL-PLACEMENT-POLICY.md)
**Files typically involved**:
- `skills/*/SKILL.md`
- `.claude/skills/*/SKILL.md`
- `.agents/skills/*/SKILL.md`
- `.cursor/skills/*/SKILL.md`
- `.agents/skills/*/agents/openai.yaml`
- `.agents/skills/*/agents/*.yaml`
- `schemas/provenance.schema.json`
- `docs/SKILL-PLACEMENT-POLICY.md`
**Example commit sequence**:
```
Copy or adapt SKILL.md to .agents/skills/{skill}/SKILL.md and/or .cursor/skills/{skill}/SKILL.md
Optionally add harness-specific openai.yaml or config files
Address review feedback to align with CONTRIBUTING template
Create or update SKILL.md in the appropriate skills directory (e.g., skills/{skill-name}/SKILL.md or .claude/skills/{skill-name}/SKILL.md)
Optionally add or update agent YAML files (e.g., .agents/skills/{skill-name}/agents/*.yaml)
Optionally update provenance or placement policy files (e.g., schemas/provenance.schema.json, docs/SKILL-PLACEMENT-POLICY.md)
```
### Add Or Update Hook
### Add Or Update Command Documentation
Adds or updates git or bash hooks to enforce workflow, quality, or security policies.
**Frequency**: ~1 times per month
**Steps**:
1. Add or update hook scripts in hooks/ or scripts/hooks/
2. Register the hook in hooks/hooks.json or similar config
3. Optionally add or update tests in tests/hooks/
**Files typically involved**:
- `hooks/*.hook`
- `hooks/hooks.json`
- `scripts/hooks/*.js`
- `tests/hooks/*.test.js`
- `.cursor/hooks.json`
**Example commit sequence**:
```
Add or update hook scripts in hooks/ or scripts/hooks/
Register the hook in hooks/hooks.json or similar config
Optionally add or update tests in tests/hooks/
```
### Address Review Feedback
Addresses code review feedback by updating documentation, scripts, or configuration for clarity, correctness, or convention alignment.
Adds or updates documentation for a command, often as part of a new feature or workflow.
**Frequency**: ~4 times per month
**Steps**:
1. Edit SKILL.md, agent, or command files to address reviewer comments
2. Update examples, headings, or configuration as requested
3. Iterate until all review feedback is resolved
1. Create or update .claude/commands/{command-name}.md
2. Optionally update localized docs (e.g., docs/zh-CN/commands/{command-name}.md, docs/tr/commands/{command-name}.md, docs/pt-BR/commands/{command-name}.md)
**Files typically involved**:
- `skills/*/SKILL.md`
- `agents/*.md`
- `commands/*.md`
- `.agents/skills/*/SKILL.md`
- `.cursor/skills/*/SKILL.md`
- `.claude/commands/*.md`
- `docs/zh-CN/commands/*.md`
- `docs/tr/commands/*.md`
- `docs/pt-BR/commands/*.md`
**Example commit sequence**:
```
Edit SKILL.md, agent, or command files to address reviewer comments
Update examples, headings, or configuration as requested
Iterate until all review feedback is resolved
Create or update .claude/commands/{command-name}.md
Optionally update localized docs (e.g., docs/zh-CN/commands/{command-name}.md, docs/tr/commands/{command-name}.md, docs/pt-BR/commands/{command-name}.md)
```
### Add Or Update Localized Documentation
Adds or updates documentation in a new or existing language (localization).
**Frequency**: ~2 times per month
**Steps**:
1. Add or update files in docs/{locale}/ (where locale is zh-CN, tr, pt-BR, etc.)
2. Update README.md to reflect new language support
**Files typically involved**:
- `docs/zh-CN/**/*`
- `docs/tr/**/*`
- `docs/pt-BR/**/*`
- `README.md`
**Example commit sequence**:
```
Add or update files in docs/{locale}/ (where locale is zh-CN, tr, pt-BR, etc.)
Update README.md to reflect new language support
```
### Update Or Add Hooks
Adds or updates hooks for validation, config protection, or workflow automation.
**Frequency**: ~2 times per month
**Steps**:
1. Edit hooks/hooks.json to add or update hook definitions
2. Create or update scripts/hooks/*.js or .ts for hook logic
3. Optionally update plugin files (e.g., .opencode/plugins/ecc-hooks.ts)
**Files typically involved**:
- `hooks/hooks.json`
- `scripts/hooks/*.js`
- `.opencode/plugins/*.ts`
**Example commit sequence**:
```
Edit hooks/hooks.json to add or update hook definitions
Create or update scripts/hooks/*.js or .ts for hook logic
Optionally update plugin files (e.g., .opencode/plugins/ecc-hooks.ts)
```
### Add Or Update Ecc Bundle
Adds or updates a set of ECC configuration, command, or skill files as a bundle.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update files in .claude/ (commands, skills, rules, team, identity, etc.)
2. Optionally update .codex/agents/*.toml and .agents/skills/*
3. Repeat for each bundle component as needed
**Files typically involved**:
- `.claude/commands/*.md`
- `.claude/skills/*/SKILL.md`
- `.claude/rules/*.md`
- `.claude/team/*.json`
- `.claude/identity.json`
- `.claude/ecc-tools.json`
- `.codex/agents/*.toml`
- `.agents/skills/*/SKILL.md`
- `.agents/skills/*/agents/*.yaml`
**Example commit sequence**:
```
Create or update files in .claude/ (commands, skills, rules, team, identity, etc.)
Optionally update .codex/agents/*.toml and .agents/skills/*
Repeat for each bundle component as needed
```

View File

@@ -0,0 +1,37 @@
---
name: add-or-update-skill-documentation
description: Workflow command scaffold for add-or-update-skill-documentation in everything-claude-code.
allowed_tools: ["Bash", "Read", "Write", "Grep", "Glob"]
---
# /add-or-update-skill-documentation
Use this workflow when working on **add-or-update-skill-documentation** in `everything-claude-code`.
## Goal
Adds a new skill or updates documentation for an existing skill, typically in the form of a SKILL.md file under skills/ or skills/*/SKILL.md.
## Common Files
- `skills/*/SKILL.md`
- `AGENTS.md`
- `README.md`
## Suggested Sequence
1. Understand the current state and failure mode before editing.
2. Make the smallest coherent change that satisfies the workflow goal.
3. Run the most relevant verification for touched files.
4. Summarize what changed and what still needs review.
## Typical Commit Signals
- Create or update skills/<skill-name>/SKILL.md
- Optionally update AGENTS.md or README.md to reflect new skill
- Optionally add architecture diagrams or implementation notes
## Notes
- Treat this as a scaffold, not a hard-coded script.
- Update the command if the workflow evolves materially.

View File

@@ -0,0 +1,40 @@
---
name: add-or-update-skill
description: Workflow command scaffold for add-or-update-skill in everything-claude-code.
allowed_tools: ["Bash", "Read", "Write", "Grep", "Glob"]
---
# /add-or-update-skill
Use this workflow when working on **add-or-update-skill** in `everything-claude-code`.
## Goal
Adds or updates a skill in the ECC system, including documentation and provenance.
## Common Files
- `skills/*/SKILL.md`
- `.claude/skills/*/SKILL.md`
- `.agents/skills/*/SKILL.md`
- `.agents/skills/*/agents/*.yaml`
- `schemas/provenance.schema.json`
- `docs/SKILL-PLACEMENT-POLICY.md`
## Suggested Sequence
1. Understand the current state and failure mode before editing.
2. Make the smallest coherent change that satisfies the workflow goal.
3. Run the most relevant verification for touched files.
4. Summarize what changed and what still needs review.
## Typical Commit Signals
- Create or update SKILL.md in the appropriate skills directory (e.g., skills/{skill-name}/SKILL.md or .claude/skills/{skill-name}/SKILL.md)
- Optionally add or update agent YAML files (e.g., .agents/skills/{skill-name}/agents/*.yaml)
- Optionally update provenance or placement policy files (e.g., schemas/provenance.schema.json, docs/SKILL-PLACEMENT-POLICY.md)
## Notes
- Treat this as a scaffold, not a hard-coded script.
- Update the command if the workflow evolves materially.

View File

@@ -14,7 +14,6 @@ Database schema changes with migration files
## Common Files
- `**/schema.*`
- `migrations/*`
## Suggested Sequence

View File

@@ -15,7 +15,6 @@ Standard feature implementation workflow
## Common Files
- `manifests/*`
- `schemas/*`
- `**/*.test.*`
- `**/api/**`

View File

@@ -2,7 +2,7 @@
"version": "1.3",
"schemaVersion": "1.0",
"generatedBy": "ecc-tools",
"generatedAt": "2026-03-20T12:07:36.496Z",
"generatedAt": "2026-03-24T10:44:06.345Z",
"repo": "https://github.com/affaan-m/everything-claude-code",
"profiles": {
"requested": "full",
@@ -150,7 +150,7 @@
".claude/enterprise/controls.md",
".claude/commands/database-migration.md",
".claude/commands/feature-development.md",
".claude/commands/add-language-rules.md"
".claude/commands/add-or-update-skill.md"
],
"packageFiles": {
"runtime-core": [
@@ -180,7 +180,7 @@
"workflow-pack": [
".claude/commands/database-migration.md",
".claude/commands/feature-development.md",
".claude/commands/add-language-rules.md"
".claude/commands/add-or-update-skill.md"
]
},
"moduleFiles": {
@@ -211,7 +211,7 @@
"workflow-pack": [
".claude/commands/database-migration.md",
".claude/commands/feature-development.md",
".claude/commands/add-language-rules.md"
".claude/commands/add-or-update-skill.md"
]
},
"files": [
@@ -297,8 +297,8 @@
},
{
"moduleId": "workflow-pack",
"path": ".claude/commands/add-language-rules.md",
"description": "Workflow command scaffold for add-language-rules."
"path": ".claude/commands/add-or-update-skill.md",
"description": "Workflow command scaffold for add-or-update-skill."
}
],
"workflows": [
@@ -311,8 +311,8 @@
"path": ".claude/commands/feature-development.md"
},
{
"command": "add-language-rules",
"path": ".claude/commands/add-language-rules.md"
"command": "add-or-update-skill",
"path": ".claude/commands/add-or-update-skill.md"
}
],
"adapters": {
@@ -322,7 +322,7 @@
"commandPaths": [
".claude/commands/database-migration.md",
".claude/commands/feature-development.md",
".claude/commands/add-language-rules.md"
".claude/commands/add-or-update-skill.md"
]
},
"codex": {

View File

@@ -10,5 +10,5 @@
"javascript"
],
"suggestedBy": "ecc-tools-repo-analysis",
"createdAt": "2026-03-20T12:07:57.119Z"
"createdAt": "2026-03-24T10:44:20.963Z"
}

View File

@@ -18,4 +18,4 @@ Use this when the task is documentation-heavy, source-sensitive, or requires bro
- Primary language: JavaScript
- Framework: Not detected
- Workflows detected: 10
- Workflows detected: 7

View File

@@ -4,7 +4,7 @@ Generated by ECC Tools from repository history. Review before treating it as a h
## Commit Workflow
- Prefer `conventional` commit messaging with prefixes such as fix, test, feat, docs.
- Prefer `conventional` commit messaging with prefixes such as feat, fix, docs, test.
- Keep new changes aligned with the existing pull-request and review flow already present in the repo.
## Architecture
@@ -26,7 +26,7 @@ Generated by ECC Tools from repository history. Review before treating it as a h
- database-migration: Database schema changes with migration files
- feature-development: Standard feature implementation workflow
- add-language-rules: Adds a new programming language to the rules system, including coding style, hooks, patterns, security, and testing guidelines.
- add-or-update-skill: Adds or updates a skill in the ECC system, including documentation and provenance.
## Review Reminder

View File

@@ -5,7 +5,7 @@ description: Development conventions and patterns for everything-claude-code. Ja
# Everything Claude Code Conventions
> Generated from [affaan-m/everything-claude-code](https://github.com/affaan-m/everything-claude-code) on 2026-03-20
> Generated from [affaan-m/everything-claude-code](https://github.com/affaan-m/everything-claude-code) on 2026-03-24
## Overview
@@ -33,14 +33,14 @@ Follow these commit message conventions based on 500 analyzed commits.
### Prefixes Used
- `fix`
- `test`
- `feat`
- `fix`
- `docs`
- `test`
### Message Guidelines
- Average message length: ~65 characters
- Average message length: ~62 characters
- Keep first line concise and descriptive
- Use imperative mood ("Add feature" not "Added feature")
@@ -48,49 +48,49 @@ Follow these commit message conventions based on 500 analyzed commits.
*Commit message example*
```text
feat(rules): add C# language support
feat: add everything-claude-code ECC bundle (.claude/commands/add-or-update-skill.md)
```
*Commit message example*
```text
chore(deps-dev): bump flatted (#675)
perf(hooks): move post-edit-format and post-edit-typecheck to strict-only (#757)
```
*Commit message example*
```text
fix: auto-detect ECC root from plugin cache when CLAUDE_PLUGIN_ROOT is unset (#547) (#691)
fix: safe Codex config sync — merge AGENTS.md + add-only MCP servers (#723)
```
*Commit message example*
```text
docs: add Antigravity setup and usage guide (#552)
docs(zh-CN): translate code block(plain text) (#753)
```
*Commit message example*
```text
merge: PR #529 — feat(skills): add documentation-lookup, bun-runtime, nextjs-turbopack; feat(agents): add rust-reviewer
security: remove supply chain risks, external promotions, and unauthorized credits
```
*Commit message example*
```text
Revert "Add Kiro IDE support (.kiro/) (#548)"
feat: add everything-claude-code ECC bundle (.claude/commands/feature-development.md)
```
*Commit message example*
```text
Add Kiro IDE support (.kiro/) (#548)
feat: add everything-claude-code ECC bundle (.claude/commands/database-migration.md)
```
*Commit message example*
```text
feat: add block-no-verify hook for Claude Code and Cursor (#649)
feat: add everything-claude-code ECC bundle (.claude/enterprise/controls.md)
```
## Architecture
@@ -196,21 +196,20 @@ Database schema changes with migration files
3. Generate/update types
**Files typically involved**:
- `**/schema.*`
- `migrations/*`
**Example commit sequence**:
```
feat: implement --with/--without selective install flags (#679)
fix: sync catalog counts with filesystem (27 agents, 113 skills, 58 commands) (#693)
feat(rules): add Rust language rules (rebased #660) (#686)
Add Turkish (tr) docs and update README (#744)
docs(zh-CN): translate code block(plain text) (#753)
fix(install): add rust, cpp, csharp to legacy language alias map (#747)
```
### Feature Development
Standard feature implementation workflow
**Frequency**: ~22 times per month
**Frequency**: ~26 times per month
**Steps**:
1. Add feature implementation
@@ -219,204 +218,136 @@ Standard feature implementation workflow
**Files typically involved**:
- `manifests/*`
- `schemas/*`
- `**/*.test.*`
- `**/api/**`
**Example commit sequence**:
```
feat(skills): add documentation-lookup, bun-runtime, nextjs-turbopack; feat(agents): add rust-reviewer
docs(skills): align documentation-lookup with CONTRIBUTING template; add cross-harness (Codex/Cursor) skill copies
fix: address PR review — skill template (When to use, How it works, Examples), bun.lock, next build note, rust-reviewer CI note, doc-lookup privacy/uncertainty
Merge pull request #736 from pvgomes/docs/add-brazilian-portuguese-translation
fix: bump plugin.json and marketplace.json to v1.9.0
Add Turkish (tr) docs and update README (#744)
```
### Add Language Rules
### Add Or Update Skill
Adds a new programming language to the rules system, including coding style, hooks, patterns, security, and testing guidelines.
**Frequency**: ~2 times per month
**Steps**:
1. Create a new directory under rules/{language}/
2. Add coding-style.md, hooks.md, patterns.md, security.md, and testing.md files with language-specific content
3. Optionally reference or link to related skills
**Files typically involved**:
- `rules/*/coding-style.md`
- `rules/*/hooks.md`
- `rules/*/patterns.md`
- `rules/*/security.md`
- `rules/*/testing.md`
**Example commit sequence**:
```
Create a new directory under rules/{language}/
Add coding-style.md, hooks.md, patterns.md, security.md, and testing.md files with language-specific content
Optionally reference or link to related skills
```
### Add New Skill
Adds a new skill to the system, documenting its workflow, triggers, and usage, often with supporting scripts.
**Frequency**: ~4 times per month
**Steps**:
1. Create a new directory under skills/{skill-name}/
2. Add SKILL.md with documentation (When to Use, How It Works, Examples, etc.)
3. Optionally add scripts or supporting files under skills/{skill-name}/scripts/
4. Address review feedback and iterate on documentation
**Files typically involved**:
- `skills/*/SKILL.md`
- `skills/*/scripts/*.sh`
- `skills/*/scripts/*.js`
**Example commit sequence**:
```
Create a new directory under skills/{skill-name}/
Add SKILL.md with documentation (When to Use, How It Works, Examples, etc.)
Optionally add scripts or supporting files under skills/{skill-name}/scripts/
Address review feedback and iterate on documentation
```
### Add New Agent
Adds a new agent to the system for code review, build resolution, or other automated tasks.
**Frequency**: ~2 times per month
**Steps**:
1. Create a new agent markdown file under agents/{agent-name}.md
2. Register the agent in AGENTS.md
3. Optionally update README.md and docs/COMMAND-AGENT-MAP.md
**Files typically involved**:
- `agents/*.md`
- `AGENTS.md`
- `README.md`
- `docs/COMMAND-AGENT-MAP.md`
**Example commit sequence**:
```
Create a new agent markdown file under agents/{agent-name}.md
Register the agent in AGENTS.md
Optionally update README.md and docs/COMMAND-AGENT-MAP.md
```
### Add New Command
Adds a new command to the system, often paired with a backing skill.
**Frequency**: ~1 times per month
**Steps**:
1. Create a new markdown file under commands/{command-name}.md
2. Optionally add or update a backing skill under skills/{skill-name}/SKILL.md
**Files typically involved**:
- `commands/*.md`
- `skills/*/SKILL.md`
**Example commit sequence**:
```
Create a new markdown file under commands/{command-name}.md
Optionally add or update a backing skill under skills/{skill-name}/SKILL.md
```
### Sync Catalog Counts
Synchronizes the documented counts of agents, skills, and commands in AGENTS.md and README.md with the actual repository state.
Adds or updates a skill in the ECC system, including documentation and provenance.
**Frequency**: ~3 times per month
**Steps**:
1. Update agent, skill, and command counts in AGENTS.md
2. Update the same counts in README.md (quick-start, comparison table, etc.)
3. Optionally update other documentation files
**Files typically involved**:
- `AGENTS.md`
- `README.md`
**Example commit sequence**:
```
Update agent, skill, and command counts in AGENTS.md
Update the same counts in README.md (quick-start, comparison table, etc.)
Optionally update other documentation files
```
### Add Cross Harness Skill Copies
Adds skill copies for different agent harnesses (e.g., Codex, Cursor, Antigravity) to ensure compatibility across platforms.
**Frequency**: ~2 times per month
**Steps**:
1. Copy or adapt SKILL.md to .agents/skills/{skill}/SKILL.md and/or .cursor/skills/{skill}/SKILL.md
2. Optionally add harness-specific openai.yaml or config files
3. Address review feedback to align with CONTRIBUTING template
1. Create or update SKILL.md in the appropriate skills directory (e.g., skills/{skill-name}/SKILL.md or .claude/skills/{skill-name}/SKILL.md)
2. Optionally add or update agent YAML files (e.g., .agents/skills/{skill-name}/agents/*.yaml)
3. Optionally update provenance or placement policy files (e.g., schemas/provenance.schema.json, docs/SKILL-PLACEMENT-POLICY.md)
**Files typically involved**:
- `skills/*/SKILL.md`
- `.claude/skills/*/SKILL.md`
- `.agents/skills/*/SKILL.md`
- `.cursor/skills/*/SKILL.md`
- `.agents/skills/*/agents/openai.yaml`
- `.agents/skills/*/agents/*.yaml`
- `schemas/provenance.schema.json`
- `docs/SKILL-PLACEMENT-POLICY.md`
**Example commit sequence**:
```
Copy or adapt SKILL.md to .agents/skills/{skill}/SKILL.md and/or .cursor/skills/{skill}/SKILL.md
Optionally add harness-specific openai.yaml or config files
Address review feedback to align with CONTRIBUTING template
Create or update SKILL.md in the appropriate skills directory (e.g., skills/{skill-name}/SKILL.md or .claude/skills/{skill-name}/SKILL.md)
Optionally add or update agent YAML files (e.g., .agents/skills/{skill-name}/agents/*.yaml)
Optionally update provenance or placement policy files (e.g., schemas/provenance.schema.json, docs/SKILL-PLACEMENT-POLICY.md)
```
### Add Or Update Hook
### Add Or Update Command Documentation
Adds or updates git or bash hooks to enforce workflow, quality, or security policies.
**Frequency**: ~1 times per month
**Steps**:
1. Add or update hook scripts in hooks/ or scripts/hooks/
2. Register the hook in hooks/hooks.json or similar config
3. Optionally add or update tests in tests/hooks/
**Files typically involved**:
- `hooks/*.hook`
- `hooks/hooks.json`
- `scripts/hooks/*.js`
- `tests/hooks/*.test.js`
- `.cursor/hooks.json`
**Example commit sequence**:
```
Add or update hook scripts in hooks/ or scripts/hooks/
Register the hook in hooks/hooks.json or similar config
Optionally add or update tests in tests/hooks/
```
### Address Review Feedback
Addresses code review feedback by updating documentation, scripts, or configuration for clarity, correctness, or convention alignment.
Adds or updates documentation for a command, often as part of a new feature or workflow.
**Frequency**: ~4 times per month
**Steps**:
1. Edit SKILL.md, agent, or command files to address reviewer comments
2. Update examples, headings, or configuration as requested
3. Iterate until all review feedback is resolved
1. Create or update .claude/commands/{command-name}.md
2. Optionally update localized docs (e.g., docs/zh-CN/commands/{command-name}.md, docs/tr/commands/{command-name}.md, docs/pt-BR/commands/{command-name}.md)
**Files typically involved**:
- `skills/*/SKILL.md`
- `agents/*.md`
- `commands/*.md`
- `.agents/skills/*/SKILL.md`
- `.cursor/skills/*/SKILL.md`
- `.claude/commands/*.md`
- `docs/zh-CN/commands/*.md`
- `docs/tr/commands/*.md`
- `docs/pt-BR/commands/*.md`
**Example commit sequence**:
```
Edit SKILL.md, agent, or command files to address reviewer comments
Update examples, headings, or configuration as requested
Iterate until all review feedback is resolved
Create or update .claude/commands/{command-name}.md
Optionally update localized docs (e.g., docs/zh-CN/commands/{command-name}.md, docs/tr/commands/{command-name}.md, docs/pt-BR/commands/{command-name}.md)
```
### Add Or Update Localized Documentation
Adds or updates documentation in a new or existing language (localization).
**Frequency**: ~2 times per month
**Steps**:
1. Add or update files in docs/{locale}/ (where locale is zh-CN, tr, pt-BR, etc.)
2. Update README.md to reflect new language support
**Files typically involved**:
- `docs/zh-CN/**/*`
- `docs/tr/**/*`
- `docs/pt-BR/**/*`
- `README.md`
**Example commit sequence**:
```
Add or update files in docs/{locale}/ (where locale is zh-CN, tr, pt-BR, etc.)
Update README.md to reflect new language support
```
### Update Or Add Hooks
Adds or updates hooks for validation, config protection, or workflow automation.
**Frequency**: ~2 times per month
**Steps**:
1. Edit hooks/hooks.json to add or update hook definitions
2. Create or update scripts/hooks/*.js or .ts for hook logic
3. Optionally update plugin files (e.g., .opencode/plugins/ecc-hooks.ts)
**Files typically involved**:
- `hooks/hooks.json`
- `scripts/hooks/*.js`
- `.opencode/plugins/*.ts`
**Example commit sequence**:
```
Edit hooks/hooks.json to add or update hook definitions
Create or update scripts/hooks/*.js or .ts for hook logic
Optionally update plugin files (e.g., .opencode/plugins/ecc-hooks.ts)
```
### Add Or Update Ecc Bundle
Adds or updates a set of ECC configuration, command, or skill files as a bundle.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update files in .claude/ (commands, skills, rules, team, identity, etc.)
2. Optionally update .codex/agents/*.toml and .agents/skills/*
3. Repeat for each bundle component as needed
**Files typically involved**:
- `.claude/commands/*.md`
- `.claude/skills/*/SKILL.md`
- `.claude/rules/*.md`
- `.claude/team/*.json`
- `.claude/identity.json`
- `.claude/ecc-tools.json`
- `.codex/agents/*.toml`
- `.agents/skills/*/SKILL.md`
- `.agents/skills/*/agents/*.yaml`
**Example commit sequence**:
```
Create or update files in .claude/ (commands, skills, rules, team, identity, etc.)
Optionally update .codex/agents/*.toml and .agents/skills/*
Repeat for each bundle component as needed
```

View File

@@ -9,7 +9,7 @@
"commandFiles": [
".claude/commands/database-migration.md",
".claude/commands/feature-development.md",
".claude/commands/add-language-rules.md"
".claude/commands/add-or-update-skill.md"
],
"updatedAt": "2026-03-20T12:07:36.496Z"
"updatedAt": "2026-03-24T10:44:06.345Z"
}

View File

@@ -13,7 +13,10 @@ pub enum MessageType {
/// Response to a query
Response { answer: String },
/// Notification of completion
Completed { summary: String, files_changed: Vec<String> },
Completed {
summary: String,
files_changed: Vec<String>,
},
/// Conflict detected (e.g., two agents editing the same file)
Conflict { file: String, description: String },
}

View File

@@ -2,7 +2,16 @@ use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct RiskThresholds {
pub review: f64,
pub confirm: f64,
pub block: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
pub db_path: PathBuf,
pub worktree_root: PathBuf,
@@ -12,6 +21,7 @@ pub struct Config {
pub heartbeat_interval_secs: u64,
pub default_agent: String,
pub theme: Theme,
pub risk_thresholds: RiskThresholds,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -32,11 +42,18 @@ impl Default for Config {
heartbeat_interval_secs: 30,
default_agent: "claude".to_string(),
theme: Theme::Dark,
risk_thresholds: Self::RISK_THRESHOLDS,
}
}
}
impl Config {
pub const RISK_THRESHOLDS: RiskThresholds = RiskThresholds {
review: 0.35,
confirm: 0.60,
block: 0.85,
};
pub fn load() -> Result<Self> {
let config_path = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
@@ -52,3 +69,40 @@ impl Config {
}
}
}
impl Default for RiskThresholds {
fn default() -> Self {
Config::RISK_THRESHOLDS
}
}
#[cfg(test)]
mod tests {
use super::Config;
#[test]
fn default_config_uses_default_risk_thresholds() {
let config = Config::default();
assert_eq!(config.risk_thresholds, Config::RISK_THRESHOLDS);
}
#[test]
fn deserialization_defaults_risk_thresholds() {
let config: Config = toml::from_str(
r#"
db_path = "/tmp/ecc2.db"
worktree_root = "/tmp/ecc-worktrees"
max_parallel_sessions = 8
max_parallel_worktrees = 6
session_timeout_secs = 3600
heartbeat_interval_secs = 30
default_agent = "claude"
theme = "Dark"
"#,
)
.expect("config should deserialize");
assert_eq!(config.risk_thresholds, Config::RISK_THRESHOLDS);
}
}

View File

@@ -1,9 +1,9 @@
mod comms;
mod config;
mod observability;
mod session;
mod tui;
mod worktree;
mod observability;
mod comms;
use anyhow::Result;
use clap::Parser;
@@ -63,10 +63,13 @@ async fn main() -> Result<()> {
Some(Commands::Dashboard) | None => {
tui::app::run(db, cfg).await?;
}
Some(Commands::Start { task, agent, worktree: use_worktree }) => {
let session_id = session::manager::create_session(
&db, &cfg, &task, &agent, use_worktree,
).await?;
Some(Commands::Start {
task,
agent,
worktree: use_worktree,
}) => {
let session_id =
session::manager::create_session(&db, &cfg, &task, &agent, use_worktree).await?;
println!("Session started: {session_id}");
}
Some(Commands::Sessions) => {

View File

@@ -1,6 +1,7 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::config::RiskThresholds;
use crate::session::store::StateStore;
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -13,36 +14,203 @@ pub struct ToolCallEvent {
pub risk_score: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RiskAssessment {
pub score: f64,
pub reasons: Vec<String>,
pub suggested_action: SuggestedAction,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SuggestedAction {
Allow,
Review,
RequireConfirmation,
Block,
}
impl ToolCallEvent {
/// Compute risk score based on tool type and input patterns.
pub fn compute_risk(tool_name: &str, input: &str) -> f64 {
let mut score: f64 = 0.0;
/// Compute risk from the tool type and input characteristics.
pub fn compute_risk(
tool_name: &str,
input: &str,
thresholds: &RiskThresholds,
) -> RiskAssessment {
let normalized_tool = tool_name.to_ascii_lowercase();
let normalized_input = input.to_ascii_lowercase();
let mut score = 0.0;
let mut reasons = Vec::new();
// Destructive tools get higher base risk
match tool_name {
"Bash" => score += 0.3,
"Write" => score += 0.2,
"Edit" => score += 0.1,
_ => score += 0.05,
let (base_score, base_reason) = base_tool_risk(&normalized_tool);
score += base_score;
if let Some(reason) = base_reason {
reasons.push(reason.to_string());
}
// Dangerous patterns in bash commands
if tool_name == "Bash" {
if input.contains("rm -rf") || input.contains("--force") {
score += 0.4;
}
if input.contains("git push") || input.contains("git reset") {
score += 0.3;
}
if input.contains("sudo") || input.contains("chmod 777") {
score += 0.5;
}
let (file_sensitivity_score, file_sensitivity_reason) =
assess_file_sensitivity(&normalized_input);
score += file_sensitivity_score;
if let Some(reason) = file_sensitivity_reason {
reasons.push(reason);
}
score.min(1.0)
let (blast_radius_score, blast_radius_reason) = assess_blast_radius(&normalized_input);
score += blast_radius_score;
if let Some(reason) = blast_radius_reason {
reasons.push(reason);
}
let (irreversibility_score, irreversibility_reason) =
assess_irreversibility(&normalized_input);
score += irreversibility_score;
if let Some(reason) = irreversibility_reason {
reasons.push(reason);
}
let score = score.clamp(0.0, 1.0);
let suggested_action = SuggestedAction::from_score(score, thresholds);
RiskAssessment {
score,
reasons,
suggested_action,
}
}
}
impl SuggestedAction {
fn from_score(score: f64, thresholds: &RiskThresholds) -> Self {
if score >= thresholds.block {
Self::Block
} else if score >= thresholds.confirm {
Self::RequireConfirmation
} else if score >= thresholds.review {
Self::Review
} else {
Self::Allow
}
}
}
fn base_tool_risk(tool_name: &str) -> (f64, Option<&'static str>) {
match tool_name {
"bash" => (
0.20,
Some("shell execution can modify local or shared state"),
),
"write" | "multiedit" => (0.15, Some("writes files directly")),
"edit" => (0.10, Some("modifies existing files")),
_ => (0.05, None),
}
}
fn assess_file_sensitivity(input: &str) -> (f64, Option<String>) {
const SECRET_PATTERNS: &[&str] = &[
".env",
"secret",
"credential",
"token",
"api_key",
"apikey",
"auth",
"id_rsa",
".pem",
".key",
];
const SHARED_INFRA_PATTERNS: &[&str] = &[
"cargo.toml",
"package.json",
"dockerfile",
".github/workflows",
"schema",
"migration",
"production",
];
if contains_any(input, SECRET_PATTERNS) {
(
0.25,
Some("targets a sensitive file or credential surface".to_string()),
)
} else if contains_any(input, SHARED_INFRA_PATTERNS) {
(
0.15,
Some("targets shared infrastructure or release-critical files".to_string()),
)
} else {
(0.0, None)
}
}
fn assess_blast_radius(input: &str) -> (f64, Option<String>) {
const LARGE_SCOPE_PATTERNS: &[&str] = &[
"**",
"/*",
"--all",
"--recursive",
"entire repo",
"all files",
"across src/",
"find ",
" xargs ",
];
const SHARED_STATE_PATTERNS: &[&str] = &[
"git push --force",
"git push -f",
"origin main",
"origin master",
"rm -rf .",
"rm -rf /",
];
if contains_any(input, SHARED_STATE_PATTERNS) {
(
0.35,
Some("has a broad blast radius across shared state or history".to_string()),
)
} else if contains_any(input, LARGE_SCOPE_PATTERNS) {
(
0.25,
Some("has a broad blast radius across multiple files or directories".to_string()),
)
} else {
(0.0, None)
}
}
fn assess_irreversibility(input: &str) -> (f64, Option<String>) {
const HIGH_IRREVERSIBILITY_PATTERNS: &[&str] = &[
"rm -rf",
"git reset --hard",
"git clean -fd",
"drop database",
"drop table",
"truncate ",
"shred ",
];
const MODERATE_IRREVERSIBILITY_PATTERNS: &[&str] =
&["rm -f", "git push --force", "git push -f", "delete from"];
if contains_any(input, HIGH_IRREVERSIBILITY_PATTERNS) {
(
0.45,
Some("includes an irreversible or destructive operation".to_string()),
)
} else if contains_any(input, MODERATE_IRREVERSIBILITY_PATTERNS) {
(
0.40,
Some("includes an irreversible or difficult-to-undo operation".to_string()),
)
} else {
(0.0, None)
}
}
fn contains_any(input: &str, patterns: &[&str]) -> bool {
patterns.iter().any(|pattern| input.contains(pattern))
}
pub fn log_tool_call(db: &StateStore, event: &ToolCallEvent) -> Result<()> {
db.send_message(
&event.session_id,
@@ -52,3 +220,72 @@ pub fn log_tool_call(db: &StateStore, event: &ToolCallEvent) -> Result<()> {
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::{SuggestedAction, ToolCallEvent};
use crate::config::Config;
#[test]
fn computes_sensitive_file_risk() {
let assessment = ToolCallEvent::compute_risk(
"Write",
"Update .env.production with rotated API token",
&Config::RISK_THRESHOLDS,
);
assert!(assessment.score >= Config::RISK_THRESHOLDS.review);
assert_eq!(assessment.suggested_action, SuggestedAction::Review);
assert!(assessment
.reasons
.iter()
.any(|reason| reason.contains("sensitive file")));
}
#[test]
fn computes_blast_radius_risk() {
let assessment = ToolCallEvent::compute_risk(
"Edit",
"Apply the same replacement across src/**/*.rs",
&Config::RISK_THRESHOLDS,
);
assert!(assessment.score >= Config::RISK_THRESHOLDS.review);
assert_eq!(assessment.suggested_action, SuggestedAction::Review);
assert!(assessment
.reasons
.iter()
.any(|reason| reason.contains("blast radius")));
}
#[test]
fn computes_irreversible_risk() {
let assessment = ToolCallEvent::compute_risk(
"Bash",
"rm -f /tmp/ecc-temp.txt",
&Config::RISK_THRESHOLDS,
);
assert!(assessment.score >= Config::RISK_THRESHOLDS.confirm);
assert_eq!(
assessment.suggested_action,
SuggestedAction::RequireConfirmation,
);
assert!(assessment
.reasons
.iter()
.any(|reason| reason.contains("irreversible")));
}
#[test]
fn blocks_combined_high_risk_operations() {
let assessment = ToolCallEvent::compute_risk(
"Bash",
"rm -rf . && git push --force origin main",
&Config::RISK_THRESHOLDS,
);
assert!(assessment.score >= Config::RISK_THRESHOLDS.block);
assert_eq!(assessment.suggested_action, SuggestedAction::Block);
}
}

View File

@@ -1,8 +1,8 @@
use anyhow::Result;
use std::fmt;
use super::{Session, SessionMetrics, SessionState};
use super::store::StateStore;
use super::{Session, SessionMetrics, SessionState};
use crate::config::Config;
use crate::worktree;

View File

@@ -1,7 +1,5 @@
pub mod daemon;
pub mod manager;
pub mod output;
pub mod runtime;
pub mod store;
use chrono::{DateTime, Utc};

View File

@@ -1,149 +0,0 @@
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex, MutexGuard};
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast;
pub const OUTPUT_BUFFER_LIMIT: usize = 1000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OutputStream {
Stdout,
Stderr,
}
impl OutputStream {
pub fn as_str(self) -> &'static str {
match self {
Self::Stdout => "stdout",
Self::Stderr => "stderr",
}
}
pub fn from_db_value(value: &str) -> Self {
match value {
"stderr" => Self::Stderr,
_ => Self::Stdout,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OutputLine {
pub stream: OutputStream,
pub text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutputEvent {
pub session_id: String,
pub line: OutputLine,
}
#[derive(Clone)]
pub struct SessionOutputStore {
capacity: usize,
buffers: Arc<Mutex<HashMap<String, VecDeque<OutputLine>>>>,
tx: broadcast::Sender<OutputEvent>,
}
impl Default for SessionOutputStore {
fn default() -> Self {
Self::new(OUTPUT_BUFFER_LIMIT)
}
}
impl SessionOutputStore {
pub fn new(capacity: usize) -> Self {
let capacity = capacity.max(1);
let (tx, _) = broadcast::channel(capacity.max(16));
Self {
capacity,
buffers: Arc::new(Mutex::new(HashMap::new())),
tx,
}
}
pub fn subscribe(&self) -> broadcast::Receiver<OutputEvent> {
self.tx.subscribe()
}
pub fn push_line(&self, session_id: &str, stream: OutputStream, text: impl Into<String>) {
let line = OutputLine {
stream,
text: text.into(),
};
{
let mut buffers = self.lock_buffers();
let buffer = buffers.entry(session_id.to_string()).or_default();
buffer.push_back(line.clone());
while buffer.len() > self.capacity {
let _ = buffer.pop_front();
}
}
let _ = self.tx.send(OutputEvent {
session_id: session_id.to_string(),
line,
});
}
pub fn replace_lines(&self, session_id: &str, lines: Vec<OutputLine>) {
let mut buffer: VecDeque<OutputLine> = lines.into_iter().collect();
while buffer.len() > self.capacity {
let _ = buffer.pop_front();
}
self.lock_buffers().insert(session_id.to_string(), buffer);
}
pub fn lines(&self, session_id: &str) -> Vec<OutputLine> {
self.lock_buffers()
.get(session_id)
.map(|buffer| buffer.iter().cloned().collect())
.unwrap_or_default()
}
fn lock_buffers(&self) -> MutexGuard<'_, HashMap<String, VecDeque<OutputLine>>> {
self.buffers
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
}
#[cfg(test)]
mod tests {
use super::{OutputStream, SessionOutputStore};
#[test]
fn ring_buffer_keeps_most_recent_lines() {
let store = SessionOutputStore::new(3);
store.push_line("session-1", OutputStream::Stdout, "line-1");
store.push_line("session-1", OutputStream::Stdout, "line-2");
store.push_line("session-1", OutputStream::Stdout, "line-3");
store.push_line("session-1", OutputStream::Stdout, "line-4");
let lines = store.lines("session-1");
let texts: Vec<_> = lines.iter().map(|line| line.text.as_str()).collect();
assert_eq!(texts, vec!["line-2", "line-3", "line-4"]);
}
#[tokio::test]
async fn pushing_output_broadcasts_events() {
let store = SessionOutputStore::new(8);
let mut rx = store.subscribe();
store.push_line("session-1", OutputStream::Stderr, "problem");
let event = rx.recv().await.expect("broadcast event");
assert_eq!(event.session_id, "session-1");
assert_eq!(event.line.stream, OutputStream::Stderr);
assert_eq!(event.line.text, "problem");
}
}

View File

@@ -1,176 +0,0 @@
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use std::process::Stdio;
use anyhow::{Context, Result};
use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
use tokio::process::Command;
use super::output::{OutputStream, SessionOutputStore};
use super::store::StateStore;
use super::SessionState;
pub async fn capture_command_output(
db_path: PathBuf,
session_id: String,
mut command: Command,
output_store: SessionOutputStore,
) -> Result<ExitStatus> {
let result = async {
let mut child = command
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.with_context(|| format!("Failed to start process for session {}", session_id))?;
update_session_state(&db_path, &session_id, SessionState::Running)?;
let stdout = child.stdout.take().context("Child stdout was not piped")?;
let stderr = child.stderr.take().context("Child stderr was not piped")?;
let stdout_task = tokio::spawn(capture_stream(
db_path.clone(),
session_id.clone(),
stdout,
OutputStream::Stdout,
output_store.clone(),
));
let stderr_task = tokio::spawn(capture_stream(
db_path.clone(),
session_id.clone(),
stderr,
OutputStream::Stderr,
output_store,
));
let status = child.wait().await?;
stdout_task.await??;
stderr_task.await??;
let final_state = if status.success() {
SessionState::Completed
} else {
SessionState::Failed
};
update_session_state(&db_path, &session_id, final_state)?;
Ok(status)
}
.await;
if result.is_err() {
let _ = update_session_state(&db_path, &session_id, SessionState::Failed);
}
result
}
async fn capture_stream<R>(
db_path: PathBuf,
session_id: String,
reader: R,
stream: OutputStream,
output_store: SessionOutputStore,
) -> Result<()>
where
R: AsyncRead + Unpin,
{
let mut lines = BufReader::new(reader).lines();
while let Some(line) = lines.next_line().await? {
output_store.push_line(&session_id, stream, line.clone());
append_output_line(&db_path, &session_id, stream, &line)?;
}
Ok(())
}
fn append_output_line(
db_path: &Path,
session_id: &str,
stream: OutputStream,
line: &str,
) -> Result<()> {
let db = StateStore::open(db_path)?;
db.append_output_line(session_id, stream, line)?;
Ok(())
}
fn update_session_state(db_path: &Path, session_id: &str, state: SessionState) -> Result<()> {
let db = StateStore::open(db_path)?;
db.update_state(session_id, &state)?;
Ok(())
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use std::env;
use anyhow::Result;
use chrono::Utc;
use tokio::process::Command;
use uuid::Uuid;
use super::capture_command_output;
use crate::session::output::{SessionOutputStore, OUTPUT_BUFFER_LIMIT};
use crate::session::store::StateStore;
use crate::session::{Session, SessionMetrics, SessionState};
#[tokio::test]
async fn capture_command_output_persists_lines_and_events() -> Result<()> {
let db_path = env::temp_dir().join(format!("ecc2-runtime-{}.db", Uuid::new_v4()));
let db = StateStore::open(&db_path)?;
let session_id = "session-1".to_string();
let now = Utc::now();
db.insert_session(&Session {
id: session_id.clone(),
task: "stream output".to_string(),
agent_type: "test".to_string(),
state: SessionState::Pending,
worktree: None,
created_at: now,
updated_at: now,
metrics: SessionMetrics::default(),
})?;
let output_store = SessionOutputStore::default();
let mut rx = output_store.subscribe();
let mut command = Command::new("/bin/sh");
command
.arg("-c")
.arg("printf 'alpha\\n'; printf 'beta\\n' >&2");
let status =
capture_command_output(db_path.clone(), session_id.clone(), command, output_store)
.await?;
assert!(status.success());
let db = StateStore::open(&db_path)?;
let session = db
.get_session(&session_id)?
.expect("session should still exist");
assert_eq!(session.state, SessionState::Completed);
let lines = db.get_output_lines(&session_id, OUTPUT_BUFFER_LIMIT)?;
let texts: HashSet<_> = lines.iter().map(|line| line.text.as_str()).collect();
assert_eq!(lines.len(), 2);
assert!(texts.contains("alpha"));
assert!(texts.contains("beta"));
let mut events = Vec::new();
while let Ok(event) = rx.try_recv() {
events.push(event.line.text);
}
assert_eq!(events.len(), 2);
assert!(events.iter().any(|line| line == "alpha"));
assert!(events.iter().any(|line| line == "beta"));
let _ = std::fs::remove_file(db_path);
Ok(())
}
}

View File

@@ -1,9 +1,7 @@
use anyhow::Result;
use rusqlite::Connection;
use std::path::Path;
use std::time::Duration;
use super::output::{OutputLine, OutputStream, OUTPUT_BUFFER_LIMIT};
use super::{Session, SessionMetrics, SessionState};
pub struct StateStore {
@@ -13,7 +11,6 @@ pub struct StateStore {
impl StateStore {
pub fn open(path: &Path) -> Result<Self> {
let conn = Connection::open(path)?;
conn.busy_timeout(Duration::from_secs(5))?;
let store = Self { conn };
store.init_schema()?;
Ok(store)
@@ -60,19 +57,9 @@ impl StateStore {
timestamp TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS session_output (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL REFERENCES sessions(id),
stream TEXT NOT NULL,
line TEXT NOT NULL,
timestamp TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_sessions_state ON sessions(state);
CREATE INDEX IF NOT EXISTS idx_tool_log_session ON tool_log(session_id);
CREATE INDEX IF NOT EXISTS idx_messages_to ON messages(to_session, read);
CREATE INDEX IF NOT EXISTS idx_session_output_session
ON session_output(session_id, id);
",
)?;
Ok(())
@@ -196,114 +183,4 @@ impl StateStore {
)?;
Ok(())
}
pub fn append_output_line(
&self,
session_id: &str,
stream: OutputStream,
line: &str,
) -> Result<()> {
let now = chrono::Utc::now().to_rfc3339();
self.conn.execute(
"INSERT INTO session_output (session_id, stream, line, timestamp)
VALUES (?1, ?2, ?3, ?4)",
rusqlite::params![session_id, stream.as_str(), line, now],
)?;
self.conn.execute(
"DELETE FROM session_output
WHERE session_id = ?1
AND id NOT IN (
SELECT id
FROM session_output
WHERE session_id = ?1
ORDER BY id DESC
LIMIT ?2
)",
rusqlite::params![session_id, OUTPUT_BUFFER_LIMIT as i64],
)?;
self.conn.execute(
"UPDATE sessions SET updated_at = ?1 WHERE id = ?2",
rusqlite::params![chrono::Utc::now().to_rfc3339(), session_id],
)?;
Ok(())
}
pub fn get_output_lines(&self, session_id: &str, limit: usize) -> Result<Vec<OutputLine>> {
let mut stmt = self.conn.prepare(
"SELECT stream, line
FROM (
SELECT id, stream, line
FROM session_output
WHERE session_id = ?1
ORDER BY id DESC
LIMIT ?2
)
ORDER BY id ASC",
)?;
let lines = stmt
.query_map(rusqlite::params![session_id, limit as i64], |row| {
let stream: String = row.get(0)?;
let text: String = row.get(1)?;
Ok(OutputLine {
stream: OutputStream::from_db_value(&stream),
text,
})
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(lines)
}
}
#[cfg(test)]
mod tests {
use std::env;
use anyhow::Result;
use chrono::Utc;
use uuid::Uuid;
use super::StateStore;
use crate::session::output::{OutputStream, OUTPUT_BUFFER_LIMIT};
use crate::session::{Session, SessionMetrics, SessionState};
#[test]
fn append_output_line_keeps_latest_buffer_window() -> Result<()> {
let db_path = env::temp_dir().join(format!("ecc2-store-{}.db", Uuid::new_v4()));
let db = StateStore::open(&db_path)?;
let now = Utc::now();
db.insert_session(&Session {
id: "session-1".to_string(),
task: "buffer output".to_string(),
agent_type: "claude".to_string(),
state: SessionState::Running,
worktree: None,
created_at: now,
updated_at: now,
metrics: SessionMetrics::default(),
})?;
for index in 0..(OUTPUT_BUFFER_LIMIT + 5) {
db.append_output_line("session-1", OutputStream::Stdout, &format!("line-{index}"))?;
}
let lines = db.get_output_lines("session-1", OUTPUT_BUFFER_LIMIT)?;
let texts: Vec<_> = lines.iter().map(|line| line.text.as_str()).collect();
assert_eq!(lines.len(), OUTPUT_BUFFER_LIMIT);
assert_eq!(texts.first().copied(), Some("line-5"));
let expected_last_line = format!("line-{}", OUTPUT_BUFFER_LIMIT + 4);
assert_eq!(texts.last().copied(), Some(expected_last_line.as_str()));
let _ = std::fs::remove_file(db_path);
Ok(())
}
}

View File

@@ -1,28 +1,20 @@
use std::collections::HashMap;
use ratatui::{
prelude::*,
widgets::{Block, Borders, List, ListItem, Paragraph, Tabs},
};
use tokio::sync::broadcast;
use crate::config::Config;
use crate::session::output::{OutputEvent, OutputLine, SessionOutputStore, OUTPUT_BUFFER_LIMIT};
use crate::session::store::StateStore;
use crate::session::{Session, SessionState};
pub struct Dashboard {
db: StateStore,
output_store: SessionOutputStore,
output_rx: broadcast::Receiver<OutputEvent>,
cfg: Config,
sessions: Vec<Session>,
session_output_cache: HashMap<String, Vec<OutputLine>>,
selected_pane: Pane,
selected_session: usize,
show_help: bool,
output_follow: bool,
output_scroll_offset: usize,
last_output_height: usize,
scroll_offset: usize,
}
#[derive(Debug, Clone, Copy, PartialEq)]
@@ -34,35 +26,19 @@ enum Pane {
impl Dashboard {
pub fn new(db: StateStore, cfg: Config) -> Self {
Self::with_output_store(db, cfg, SessionOutputStore::default())
}
pub fn with_output_store(
db: StateStore,
_cfg: Config,
output_store: SessionOutputStore,
) -> Self {
let sessions = db.list_sessions().unwrap_or_default();
let output_rx = output_store.subscribe();
let mut dashboard = Self {
Self {
db,
output_store,
output_rx,
cfg,
sessions,
session_output_cache: HashMap::new(),
selected_pane: Pane::Sessions,
selected_session: 0,
show_help: false,
output_follow: true,
output_scroll_offset: 0,
last_output_height: 0,
};
dashboard.sync_selected_output();
dashboard
scroll_offset: 0,
}
}
pub fn render(&mut self, frame: &mut Frame) {
pub fn render(&self, frame: &mut Frame) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
@@ -173,21 +149,12 @@ impl Dashboard {
frame.render_widget(list, area);
}
fn render_output(&mut self, frame: &mut Frame, area: Rect) {
self.sync_output_scroll(area.height.saturating_sub(2) as usize);
let content = if self.sessions.get(self.selected_session).is_some() {
let lines = self.selected_output_lines();
if lines.is_empty() {
"Waiting for session output...".to_string()
} else {
lines
.iter()
.map(|line| line.text.as_str())
.collect::<Vec<_>>()
.join("\n")
}
fn render_output(&self, frame: &mut Frame, area: Rect) {
let content = if let Some(session) = self.sessions.get(self.selected_session) {
format!(
"Agent output for session {}...\n\n(Live streaming coming soon)",
session.id
)
} else {
"No sessions. Press 'n' to start one.".to_string()
};
@@ -198,14 +165,12 @@ impl Dashboard {
Style::default()
};
let paragraph = Paragraph::new(content)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Output ")
.border_style(border_style),
)
.scroll((self.output_scroll_offset as u16, 0));
let paragraph = Paragraph::new(content).block(
Block::default()
.borders(Borders::ALL)
.title(" Output ")
.border_style(border_style),
);
frame.render_widget(paragraph, area);
}
@@ -286,38 +251,16 @@ impl Dashboard {
pub fn scroll_down(&mut self) {
if self.selected_pane == Pane::Sessions && !self.sessions.is_empty() {
self.selected_session = (self.selected_session + 1).min(self.sessions.len() - 1);
self.reset_output_view();
self.sync_selected_output();
} else if self.selected_pane == Pane::Output {
let max_scroll = self.max_output_scroll();
if self.output_follow {
return;
}
if self.output_scroll_offset >= max_scroll.saturating_sub(1) {
self.output_follow = true;
self.output_scroll_offset = max_scroll;
} else {
self.output_scroll_offset = self.output_scroll_offset.saturating_add(1);
}
} else {
self.scroll_offset = self.scroll_offset.saturating_add(1);
}
}
pub fn scroll_up(&mut self) {
if self.selected_pane == Pane::Sessions {
self.selected_session = self.selected_session.saturating_sub(1);
self.reset_output_view();
self.sync_selected_output();
} else if self.selected_pane == Pane::Output {
if self.output_follow {
self.output_follow = false;
self.output_scroll_offset = self.max_output_scroll();
}
self.output_scroll_offset = self.output_scroll_offset.saturating_sub(1);
} else {
self.output_scroll_offset = self.output_scroll_offset.saturating_sub(1);
self.scroll_offset = self.scroll_offset.saturating_sub(1);
}
}
@@ -335,8 +278,6 @@ impl Dashboard {
pub fn refresh(&mut self) {
self.sessions = self.db.list_sessions().unwrap_or_default();
self.clamp_selected_session();
self.sync_selected_output();
}
pub fn toggle_help(&mut self) {
@@ -344,169 +285,7 @@ impl Dashboard {
}
pub async fn tick(&mut self) {
loop {
match self.output_rx.try_recv() {
Ok(_event) => {}
Err(broadcast::error::TryRecvError::Empty) => break,
Err(broadcast::error::TryRecvError::Lagged(_)) => continue,
Err(broadcast::error::TryRecvError::Closed) => break,
}
}
// Periodic refresh every few ticks
self.sessions = self.db.list_sessions().unwrap_or_default();
self.clamp_selected_session();
self.sync_selected_output();
}
fn clamp_selected_session(&mut self) {
if self.sessions.is_empty() {
self.selected_session = 0;
} else {
self.selected_session = self.selected_session.min(self.sessions.len() - 1);
}
}
fn sync_selected_output(&mut self) {
let Some(session_id) = self.selected_session_id().map(ToOwned::to_owned) else {
return;
};
match self.db.get_output_lines(&session_id, OUTPUT_BUFFER_LIMIT) {
Ok(lines) => {
self.output_store.replace_lines(&session_id, lines.clone());
self.session_output_cache.insert(session_id, lines);
}
Err(error) => {
tracing::warn!("Failed to load session output: {error}");
}
}
}
fn selected_session_id(&self) -> Option<&str> {
self.sessions
.get(self.selected_session)
.map(|session| session.id.as_str())
}
fn selected_output_lines(&self) -> &[OutputLine] {
self.selected_session_id()
.and_then(|session_id| self.session_output_cache.get(session_id))
.map(Vec::as_slice)
.unwrap_or(&[])
}
fn sync_output_scroll(&mut self, viewport_height: usize) {
self.last_output_height = viewport_height.max(1);
let max_scroll = self.max_output_scroll();
if self.output_follow {
self.output_scroll_offset = max_scroll;
} else {
self.output_scroll_offset = self.output_scroll_offset.min(max_scroll);
}
}
fn max_output_scroll(&self) -> usize {
self.selected_output_lines()
.len()
.saturating_sub(self.last_output_height.max(1))
}
fn reset_output_view(&mut self) {
self.output_follow = true;
self.output_scroll_offset = 0;
}
#[cfg(test)]
fn selected_output_text(&self) -> String {
self.selected_output_lines()
.iter()
.map(|line| line.text.clone())
.collect::<Vec<_>>()
.join("\n")
}
}
#[cfg(test)]
mod tests {
use std::env;
use anyhow::Result;
use chrono::Utc;
use uuid::Uuid;
use super::{Dashboard, Pane};
use crate::config::Config;
use crate::session::output::OutputStream;
use crate::session::store::StateStore;
use crate::session::{Session, SessionMetrics, SessionState};
#[test]
fn refresh_loads_selected_session_output_and_follows_tail() -> Result<()> {
let db_path = env::temp_dir().join(format!("ecc2-dashboard-{}.db", Uuid::new_v4()));
let db = StateStore::open(&db_path)?;
let now = Utc::now();
db.insert_session(&Session {
id: "session-1".to_string(),
task: "tail output".to_string(),
agent_type: "claude".to_string(),
state: SessionState::Running,
worktree: None,
created_at: now,
updated_at: now,
metrics: SessionMetrics::default(),
})?;
for index in 0..12 {
db.append_output_line("session-1", OutputStream::Stdout, &format!("line {index}"))?;
}
let mut dashboard = Dashboard::new(db, Config::default());
dashboard.selected_pane = Pane::Output;
dashboard.refresh();
dashboard.sync_output_scroll(4);
assert_eq!(dashboard.output_scroll_offset, 8);
assert!(dashboard.selected_output_text().contains("line 11"));
let _ = std::fs::remove_file(db_path);
Ok(())
}
#[test]
fn scrolling_up_disables_follow_mode() -> Result<()> {
let db_path = env::temp_dir().join(format!("ecc2-dashboard-{}.db", Uuid::new_v4()));
let db = StateStore::open(&db_path)?;
let now = Utc::now();
db.insert_session(&Session {
id: "session-1".to_string(),
task: "inspect output".to_string(),
agent_type: "claude".to_string(),
state: SessionState::Running,
worktree: None,
created_at: now,
updated_at: now,
metrics: SessionMetrics::default(),
})?;
for index in 0..6 {
db.append_output_line("session-1", OutputStream::Stdout, &format!("line {index}"))?;
}
let mut dashboard = Dashboard::new(db, Config::default());
dashboard.selected_pane = Pane::Output;
dashboard.refresh();
dashboard.sync_output_scroll(3);
dashboard.scroll_up();
assert!(!dashboard.output_follow);
assert_eq!(dashboard.output_scroll_offset, 2);
let _ = std::fs::remove_file(db_path);
Ok(())
}
}

View File

@@ -28,7 +28,11 @@ pub fn create_for_session(session_id: &str, cfg: &Config) -> Result<WorktreeInfo
anyhow::bail!("git worktree add failed: {stderr}");
}
tracing::info!("Created worktree at {} on branch {}", path.display(), branch);
tracing::info!(
"Created worktree at {} on branch {}",
path.display(),
branch
);
Ok(WorktreeInfo {
path,