Compare commits

...

47 Commits

Author SHA1 Message Date
ecc-tools[bot]
b965cfbd00 feat: add everything-claude-code ECC bundle (.claude/commands/add-or-update-command-doc.md) 2026-03-24 10:44:57 +00:00
ecc-tools[bot]
61d9afe780 feat: add everything-claude-code ECC bundle (.claude/commands/feature-development.md) 2026-03-24 10:44:56 +00:00
ecc-tools[bot]
0aaaea6db0 feat: add everything-claude-code ECC bundle (.claude/commands/database-migration.md) 2026-03-24 10:44:55 +00:00
ecc-tools[bot]
545c8e2849 feat: add everything-claude-code ECC bundle (.claude/enterprise/controls.md) 2026-03-24 10:44:54 +00:00
ecc-tools[bot]
94175eddd0 feat: add everything-claude-code ECC bundle (.claude/team/everything-claude-code-team-config.json) 2026-03-24 10:44:53 +00:00
ecc-tools[bot]
2b6d9a8989 feat: add everything-claude-code ECC bundle (.claude/research/everything-claude-code-research-playbook.md) 2026-03-24 10:44:52 +00:00
ecc-tools[bot]
6bf3191a1d feat: add everything-claude-code ECC bundle (.claude/rules/everything-claude-code-guardrails.md) 2026-03-24 10:44:52 +00:00
ecc-tools[bot]
4e96a3468a feat: add everything-claude-code ECC bundle (.codex/agents/docs-researcher.toml) 2026-03-24 10:44:51 +00:00
ecc-tools[bot]
bd220783ac feat: add everything-claude-code ECC bundle (.codex/agents/reviewer.toml) 2026-03-24 10:44:50 +00:00
ecc-tools[bot]
3ef599d5e5 feat: add everything-claude-code ECC bundle (.codex/agents/explorer.toml) 2026-03-24 10:44:49 +00:00
ecc-tools[bot]
10e91c2592 feat: add everything-claude-code ECC bundle (.claude/identity.json) 2026-03-24 10:44:48 +00:00
ecc-tools[bot]
bb06cc8d53 feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/agents/openai.yaml) 2026-03-24 10:44:47 +00:00
ecc-tools[bot]
836573a930 feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/SKILL.md) 2026-03-24 10:44:46 +00:00
ecc-tools[bot]
2e43654d47 feat: add everything-claude-code ECC bundle (.claude/skills/everything-claude-code/SKILL.md) 2026-03-24 10:44:45 +00:00
ecc-tools[bot]
515c39fa51 feat: add everything-claude-code ECC bundle (.claude/ecc-tools.json) 2026-03-24 10:44:44 +00:00
ecc-tools[bot]
d504523961 feat: add everything-claude-code ECC bundle (.claude/commands/add-or-update-skill.md) 2026-03-24 10:44:12 +00:00
ecc-tools[bot]
0c5cf99ffa feat: add everything-claude-code ECC bundle (.claude/commands/feature-development.md) 2026-03-24 10:44:11 +00:00
ecc-tools[bot]
86a0449376 feat: add everything-claude-code ECC bundle (.claude/commands/database-migration.md) 2026-03-24 10:44:10 +00:00
ecc-tools[bot]
1cd1f303fc feat: add everything-claude-code ECC bundle (.claude/enterprise/controls.md) 2026-03-24 10:44:09 +00:00
ecc-tools[bot]
3dead9ac8c feat: add everything-claude-code ECC bundle (.claude/team/everything-claude-code-team-config.json) 2026-03-24 10:44:09 +00:00
ecc-tools[bot]
0c2d1a3203 feat: add everything-claude-code ECC bundle (.claude/research/everything-claude-code-research-playbook.md) 2026-03-24 10:44:08 +00:00
ecc-tools[bot]
799abcf26e feat: add everything-claude-code ECC bundle (.claude/rules/everything-claude-code-guardrails.md) 2026-03-24 10:44:07 +00:00
ecc-tools[bot]
ad96ba1ae4 feat: add everything-claude-code ECC bundle (.codex/agents/docs-researcher.toml) 2026-03-24 10:44:06 +00:00
ecc-tools[bot]
a66a7d9e2b feat: add everything-claude-code ECC bundle (.codex/agents/reviewer.toml) 2026-03-24 10:44:05 +00:00
ecc-tools[bot]
220fdca4ae feat: add everything-claude-code ECC bundle (.codex/agents/explorer.toml) 2026-03-24 10:44:04 +00:00
ecc-tools[bot]
a84f4689df feat: add everything-claude-code ECC bundle (.claude/identity.json) 2026-03-24 10:44:03 +00:00
ecc-tools[bot]
656d12ddf2 feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/agents/openai.yaml) 2026-03-24 10:44:02 +00:00
ecc-tools[bot]
d522d64cd8 feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/SKILL.md) 2026-03-24 10:44:01 +00:00
ecc-tools[bot]
55801750e8 feat: add everything-claude-code ECC bundle (.claude/skills/everything-claude-code/SKILL.md) 2026-03-24 10:44:00 +00:00
ecc-tools[bot]
d7a30fcc3b feat: add everything-claude-code ECC bundle (.claude/ecc-tools.json) 2026-03-24 10:43:59 +00:00
ecc-tools[bot]
6cf6b7cf95 feat: add everything-claude-code ECC bundle (.claude/commands/add-or-update-skill-documentation.md) 2026-03-24 10:43:29 +00:00
ecc-tools[bot]
fa86d38b8b feat: add everything-claude-code ECC bundle (.claude/commands/feature-development.md) 2026-03-24 10:43:28 +00:00
ecc-tools[bot]
bf1d1af149 feat: add everything-claude-code ECC bundle (.claude/commands/database-migration.md) 2026-03-24 10:43:27 +00:00
ecc-tools[bot]
d298141067 feat: add everything-claude-code ECC bundle (.claude/enterprise/controls.md) 2026-03-24 10:43:26 +00:00
ecc-tools[bot]
a2c95ce334 feat: add everything-claude-code ECC bundle (.claude/team/everything-claude-code-team-config.json) 2026-03-24 10:43:25 +00:00
ecc-tools[bot]
49f11ea0d4 feat: add everything-claude-code ECC bundle (.claude/research/everything-claude-code-research-playbook.md) 2026-03-24 10:43:24 +00:00
ecc-tools[bot]
e0f2a1d3f9 feat: add everything-claude-code ECC bundle (.claude/rules/everything-claude-code-guardrails.md) 2026-03-24 10:43:24 +00:00
ecc-tools[bot]
7d66242d75 feat: add everything-claude-code ECC bundle (.codex/agents/docs-researcher.toml) 2026-03-24 10:43:23 +00:00
ecc-tools[bot]
3f0d15e04c feat: add everything-claude-code ECC bundle (.codex/agents/reviewer.toml) 2026-03-24 10:43:22 +00:00
ecc-tools[bot]
debc2f542f feat: add everything-claude-code ECC bundle (.codex/agents/explorer.toml) 2026-03-24 10:43:21 +00:00
ecc-tools[bot]
1fd24654eb feat: add everything-claude-code ECC bundle (.claude/identity.json) 2026-03-24 10:43:19 +00:00
ecc-tools[bot]
e2adf4608f feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/agents/openai.yaml) 2026-03-24 10:43:18 +00:00
ecc-tools[bot]
74db820e44 feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/SKILL.md) 2026-03-24 10:43:18 +00:00
ecc-tools[bot]
b984d2dd8c feat: add everything-claude-code ECC bundle (.claude/skills/everything-claude-code/SKILL.md) 2026-03-24 10:43:17 +00:00
ecc-tools[bot]
b766007818 feat: add everything-claude-code ECC bundle (.claude/ecc-tools.json) 2026-03-24 10:43:16 +00:00
Affaan Mustafa
df8c951ec2 feat(ecc2): add crash resume session recovery 2026-03-24 03:39:53 -07:00
Affaan Mustafa
b032846806 feat: scaffold ECC 2.0 Rust TUI — agentic IDE control plane
Initial scaffold for ECC 2.0, a terminal-native agentic IDE built with
Ratatui. Compiles to a 3.4MB single binary.

Core modules:
- Session manager with SQLite-backed state store
- TUI dashboard with split-pane layout (sessions, output, metrics)
- Worktree orchestration (auto-create per agent session)
- Observability with tool call risk scoring
- Inter-agent communication via SQLite mailbox
- Background daemon with heartbeat monitoring
- CLI with start/stop/sessions/status/daemon subcommands

Tech stack: Rust + Ratatui + Crossterm + Tokio + rusqlite + git2 + clap
2026-03-22 15:48:19 -07:00
28 changed files with 3753 additions and 367 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,105 @@ 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 Command Doc
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 documentation for a command, typically in Markdown under a language or locale-specific docs directory.
**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
1. Create or update a Markdown file for the command in the appropriate docs directory (e.g., docs/zh-CN/commands/ or docs/tr/commands/).
2. Commit the new or changed file with a message referencing the command.
3. Optionally update README or language count if adding a new language.
**Files typically involved**:
- `AGENTS.md`
- `README.md`
- `docs/zh-CN/commands/*.md`
- `docs/tr/commands/*.md`
- `docs/pt-BR/commands/*.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
Create or update a Markdown file for the command in the appropriate docs directory (e.g., docs/zh-CN/commands/ or docs/tr/commands/).
Commit the new or changed file with a message referencing the command.
Optionally update README or language count if adding a new language.
```
### Add Cross Harness Skill Copies
### Add Or Update Skill Doc
Adds skill copies for different agent harnesses (e.g., Codex, Cursor, Antigravity) to ensure compatibility across platforms.
Adds or updates documentation for a skill, typically in SKILL.md under a language or locale-specific docs directory.
**Frequency**: ~3 times per month
**Steps**:
1. Create or update a SKILL.md file for the skill in the appropriate docs directory (e.g., docs/zh-CN/skills/, docs/tr/skills/, docs/pt-BR/skills/).
2. Commit the new or changed file with a message referencing the skill.
**Files typically involved**:
- `docs/zh-CN/skills/*/SKILL.md`
- `docs/tr/skills/*/SKILL.md`
- `docs/pt-BR/skills/*/SKILL.md`
**Example commit sequence**:
```
Create or update a SKILL.md file for the skill in the appropriate docs directory (e.g., docs/zh-CN/skills/, docs/tr/skills/, docs/pt-BR/skills/).
Commit the new or changed file with a message referencing the skill.
```
### Add Or Update Locale Docs
Adds or updates a full set of localized documentation for a new or existing language, including agents, commands, skills, and guides.
**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 multiple Markdown files under a new or existing language directory (e.g., docs/tr/, docs/pt-BR/, docs/zh-CN/).
2. Update README.md to increment supported language count and add references.
3. Commit all new or changed files.
**Files typically involved**:
- `.agents/skills/*/SKILL.md`
- `.cursor/skills/*/SKILL.md`
- `.agents/skills/*/agents/openai.yaml`
- `docs/tr/**/*`
- `docs/pt-BR/**/*`
- `docs/zh-CN/**/*`
- `README.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 multiple Markdown files under a new or existing language directory (e.g., docs/tr/, docs/pt-BR/, docs/zh-CN/).
Update README.md to increment supported language count and add references.
Commit all new or changed files.
```
### Add Or Update Hook
### Add Or Update Ecc Bundle Command
Adds or updates git or bash hooks to enforce workflow, quality, or security policies.
Adds or updates ECC bundle command documentation or configuration, typically in .claude/commands/ or related ECC config directories.
**Frequency**: ~1 times per month
**Frequency**: ~2 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/
1. Create or update a Markdown file in .claude/commands/ for the new or updated command.
2. Optionally update related config files (e.g., .claude/ecc-tools.json, .claude/identity.json).
3. Commit the changes.
**Files typically involved**:
- `hooks/*.hook`
- `hooks/hooks.json`
- `scripts/hooks/*.js`
- `tests/hooks/*.test.js`
- `.cursor/hooks.json`
- `.claude/commands/*.md`
- `.claude/ecc-tools.json`
- `.claude/identity.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.
**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
**Files typically involved**:
- `skills/*/SKILL.md`
- `agents/*.md`
- `commands/*.md`
- `.agents/skills/*/SKILL.md`
- `.cursor/skills/*/SKILL.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 a Markdown file in .claude/commands/ for the new or updated command.
Optionally update related config files (e.g., .claude/ecc-tools.json, .claude/identity.json).
Commit the changes.
```

View File

@@ -0,0 +1,37 @@
---
name: add-or-update-command-doc
description: Workflow command scaffold for add-or-update-command-doc in everything-claude-code.
allowed_tools: ["Bash", "Read", "Write", "Grep", "Glob"]
---
# /add-or-update-command-doc
Use this workflow when working on **add-or-update-command-doc** in `everything-claude-code`.
## Goal
Adds or updates documentation for a command, typically in Markdown under a language or locale-specific docs directory.
## Common Files
- `docs/zh-CN/commands/*.md`
- `docs/tr/commands/*.md`
- `docs/pt-BR/commands/*.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 a Markdown file for the command in the appropriate docs directory (e.g., docs/zh-CN/commands/ or docs/tr/commands/).
- Commit the new or changed file with a message referencing the command.
- Optionally update README or language count if adding a new language.
## Notes
- Treat this as a scaffold, not a hard-coded script.
- Update the command if the workflow evolves materially.

View File

@@ -0,0 +1,38 @@
---
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, sometimes with translations in docs/xx/skills/*/SKILL.md.
## Common Files
- `skills/*/SKILL.md`
- `docs/zh-CN/skills/*/SKILL.md`
- `docs/tr/skills/*/SKILL.md`
- `docs/pt-BR/skills/*/SKILL.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 docs/xx/skills/<skill-name>/SKILL.md for translations
- Commit with a message referencing the skill and a summary of changes
## Notes
- Treat this as a scaffold, not a hard-coded script.
- Update the command if the workflow evolves materially.

View File

@@ -0,0 +1,36 @@
---
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 a new skill or updates documentation for an existing skill.
## Common Files
- `skills/*/SKILL.md`
- `docs/zh-CN/skills/*/SKILL.md`
- `docs/tr/skills/*/SKILL.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 relevant skills directory.
- Optionally add architecture diagrams, implementation notes, or integration guidance.
## 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:32.276Z",
"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-command-doc.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-command-doc.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-command-doc.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-command-doc.md",
"description": "Workflow command scaffold for add-or-update-command-doc."
}
],
"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-command-doc",
"path": ".claude/commands/add-or-update-command-doc.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-command-doc.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:42.288Z"
}

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: 6

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-command-doc: Adds or updates documentation for a command, typically in Markdown under a language or locale-specific docs directory.
## 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,105 @@ 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 Command Doc
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 documentation for a command, typically in Markdown under a language or locale-specific docs directory.
**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
1. Create or update a Markdown file for the command in the appropriate docs directory (e.g., docs/zh-CN/commands/ or docs/tr/commands/).
2. Commit the new or changed file with a message referencing the command.
3. Optionally update README or language count if adding a new language.
**Files typically involved**:
- `AGENTS.md`
- `README.md`
- `docs/zh-CN/commands/*.md`
- `docs/tr/commands/*.md`
- `docs/pt-BR/commands/*.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
Create or update a Markdown file for the command in the appropriate docs directory (e.g., docs/zh-CN/commands/ or docs/tr/commands/).
Commit the new or changed file with a message referencing the command.
Optionally update README or language count if adding a new language.
```
### Add Cross Harness Skill Copies
### Add Or Update Skill Doc
Adds skill copies for different agent harnesses (e.g., Codex, Cursor, Antigravity) to ensure compatibility across platforms.
Adds or updates documentation for a skill, typically in SKILL.md under a language or locale-specific docs directory.
**Frequency**: ~3 times per month
**Steps**:
1. Create or update a SKILL.md file for the skill in the appropriate docs directory (e.g., docs/zh-CN/skills/, docs/tr/skills/, docs/pt-BR/skills/).
2. Commit the new or changed file with a message referencing the skill.
**Files typically involved**:
- `docs/zh-CN/skills/*/SKILL.md`
- `docs/tr/skills/*/SKILL.md`
- `docs/pt-BR/skills/*/SKILL.md`
**Example commit sequence**:
```
Create or update a SKILL.md file for the skill in the appropriate docs directory (e.g., docs/zh-CN/skills/, docs/tr/skills/, docs/pt-BR/skills/).
Commit the new or changed file with a message referencing the skill.
```
### Add Or Update Locale Docs
Adds or updates a full set of localized documentation for a new or existing language, including agents, commands, skills, and guides.
**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 multiple Markdown files under a new or existing language directory (e.g., docs/tr/, docs/pt-BR/, docs/zh-CN/).
2. Update README.md to increment supported language count and add references.
3. Commit all new or changed files.
**Files typically involved**:
- `.agents/skills/*/SKILL.md`
- `.cursor/skills/*/SKILL.md`
- `.agents/skills/*/agents/openai.yaml`
- `docs/tr/**/*`
- `docs/pt-BR/**/*`
- `docs/zh-CN/**/*`
- `README.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 multiple Markdown files under a new or existing language directory (e.g., docs/tr/, docs/pt-BR/, docs/zh-CN/).
Update README.md to increment supported language count and add references.
Commit all new or changed files.
```
### Add Or Update Hook
### Add Or Update Ecc Bundle Command
Adds or updates git or bash hooks to enforce workflow, quality, or security policies.
Adds or updates ECC bundle command documentation or configuration, typically in .claude/commands/ or related ECC config directories.
**Frequency**: ~1 times per month
**Frequency**: ~2 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/
1. Create or update a Markdown file in .claude/commands/ for the new or updated command.
2. Optionally update related config files (e.g., .claude/ecc-tools.json, .claude/identity.json).
3. Commit the changes.
**Files typically involved**:
- `hooks/*.hook`
- `hooks/hooks.json`
- `scripts/hooks/*.js`
- `tests/hooks/*.test.js`
- `.cursor/hooks.json`
- `.claude/commands/*.md`
- `.claude/ecc-tools.json`
- `.claude/identity.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.
**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
**Files typically involved**:
- `skills/*/SKILL.md`
- `agents/*.md`
- `commands/*.md`
- `.agents/skills/*/SKILL.md`
- `.cursor/skills/*/SKILL.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 a Markdown file in .claude/commands/ for the new or updated command.
Optionally update related config files (e.g., .claude/ecc-tools.json, .claude/identity.json).
Commit the changes.
```

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-command-doc.md"
],
"updatedAt": "2026-03-20T12:07:36.496Z"
"updatedAt": "2026-03-24T10:44:32.276Z"
}

3
.gitignore vendored
View File

@@ -83,6 +83,9 @@ temp/
*.bak
*.backup
# Rust build artifacts
ecc2/target/
# Bootstrap pipeline outputs
# Generated lock files in tool subdirectories
.opencode/package-lock.json

2017
ecc2/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

53
ecc2/Cargo.toml Normal file
View File

@@ -0,0 +1,53 @@
[package]
name = "ecc-tui"
version = "0.1.0"
edition = "2021"
description = "ECC 2.0 — Agentic IDE control plane with TUI dashboard"
license = "MIT"
authors = ["Affaan Mustafa <me@affaanmustafa.com>"]
repository = "https://github.com/affaan-m/everything-claude-code"
[dependencies]
# TUI
ratatui = "0.29"
crossterm = "0.28"
# Async runtime
tokio = { version = "1", features = ["full"] }
# State store
rusqlite = { version = "0.32", features = ["bundled"] }
# Git integration
git2 = "0.19"
# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
# CLI
clap = { version = "4", features = ["derive"] }
# Logging & tracing
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Error handling
anyhow = "1"
thiserror = "2"
libc = "0.2"
# Time
chrono = { version = "0.4", features = ["serde"] }
# UUID for session IDs
uuid = { version = "1", features = ["v4"] }
# Directory paths
dirs = "6"
[profile.release]
lto = true
codegen-units = 1
strip = true

33
ecc2/src/comms/mod.rs Normal file
View File

@@ -0,0 +1,33 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::session::store::StateStore;
/// Message types for inter-agent communication.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MessageType {
/// Task handoff from one agent to another
TaskHandoff { task: String, context: String },
/// Agent requesting information from another
Query { question: String },
/// Response to a query
Response { answer: String },
/// Notification of completion
Completed { summary: String, files_changed: Vec<String> },
/// Conflict detected (e.g., two agents editing the same file)
Conflict { file: String, description: String },
}
/// Send a structured message between sessions.
pub fn send(db: &StateStore, from: &str, to: &str, msg: &MessageType) -> Result<()> {
let content = serde_json::to_string(msg)?;
let msg_type = match msg {
MessageType::TaskHandoff { .. } => "task_handoff",
MessageType::Query { .. } => "query",
MessageType::Response { .. } => "response",
MessageType::Completed { .. } => "completed",
MessageType::Conflict { .. } => "conflict",
};
db.send_message(from, to, &content, msg_type)?;
Ok(())
}

54
ecc2/src/config/mod.rs Normal file
View File

@@ -0,0 +1,54 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub db_path: PathBuf,
pub worktree_root: PathBuf,
pub max_parallel_sessions: usize,
pub max_parallel_worktrees: usize,
pub session_timeout_secs: u64,
pub heartbeat_interval_secs: u64,
pub default_agent: String,
pub theme: Theme,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Theme {
Dark,
Light,
}
impl Default for Config {
fn default() -> Self {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
Self {
db_path: home.join(".claude").join("ecc2.db"),
worktree_root: PathBuf::from("/tmp/ecc-worktrees"),
max_parallel_sessions: 8,
max_parallel_worktrees: 6,
session_timeout_secs: 3600,
heartbeat_interval_secs: 30,
default_agent: "claude".to_string(),
theme: Theme::Dark,
}
}
}
impl Config {
pub fn load() -> Result<Self> {
let config_path = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".claude")
.join("ecc2.toml");
if config_path.exists() {
let content = std::fs::read_to_string(&config_path)?;
let config: Config = toml::from_str(&content)?;
Ok(config)
} else {
Ok(Config::default())
}
}
}

122
ecc2/src/main.rs Normal file
View File

@@ -0,0 +1,122 @@
mod comms;
mod config;
mod observability;
mod session;
mod tui;
mod worktree;
use anyhow::Result;
use clap::Parser;
use tracing_subscriber::EnvFilter;
#[derive(Parser, Debug)]
#[command(name = "ecc", version, about = "ECC 2.0 — Agentic IDE control plane")]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(clap::Subcommand, Debug)]
enum Commands {
/// Launch the TUI dashboard
Dashboard,
/// Start a new agent session
Start {
/// Task description for the agent
#[arg(short, long)]
task: String,
/// Agent type (claude, codex, custom)
#[arg(short, long, default_value = "claude")]
agent: String,
/// Create a dedicated worktree for this session
#[arg(short, long)]
worktree: bool,
},
/// List active sessions
Sessions,
/// Show session details
Status {
/// Session ID or alias
session_id: Option<String>,
},
/// Stop a running session
Stop {
/// Session ID or alias
session_id: String,
},
/// Resume a failed or stopped session
Resume {
/// Session ID or alias
session_id: String,
},
/// Run as background daemon
Daemon,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
let cli = Cli::parse();
let cfg = config::Config::load()?;
let db = session::store::StateStore::open(&cfg.db_path)?;
match cli.command {
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?;
println!("Session started: {session_id}");
}
Some(Commands::Sessions) => {
let sessions = session::manager::list_sessions(&db)?;
for s in sessions {
println!("{} [{}] {}", s.id, s.state, s.task);
}
}
Some(Commands::Status { session_id }) => {
let id = session_id.unwrap_or_else(|| "latest".to_string());
let status = session::manager::get_status(&db, &id)?;
println!("{status}");
}
Some(Commands::Stop { session_id }) => {
session::manager::stop_session(&db, &session_id).await?;
println!("Session stopped: {session_id}");
}
Some(Commands::Resume { session_id }) => {
let resumed_id = session::manager::resume_session(&db, &session_id).await?;
println!("Session resumed: {resumed_id}");
}
Some(Commands::Daemon) => {
println!("Starting ECC daemon...");
session::daemon::run(db, cfg).await?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cli_parses_resume_command() {
let cli = Cli::try_parse_from(["ecc", "resume", "deadbeef"])
.expect("resume subcommand should parse");
match cli.command {
Some(Commands::Resume { session_id }) => assert_eq!(session_id, "deadbeef"),
_ => panic!("expected resume subcommand"),
}
}
}

View File

@@ -0,0 +1,54 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::session::store::StateStore;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallEvent {
pub session_id: String,
pub tool_name: String,
pub input_summary: String,
pub output_summary: String,
pub duration_ms: u64,
pub risk_score: f64,
}
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;
// Destructive tools get higher base risk
match tool_name {
"Bash" => score += 0.3,
"Write" => score += 0.2,
"Edit" => score += 0.1,
_ => score += 0.05,
}
// 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;
}
}
score.min(1.0)
}
}
pub fn log_tool_call(db: &StateStore, event: &ToolCallEvent) -> Result<()> {
db.send_message(
&event.session_id,
"observability",
&serde_json::to_string(event)?,
"tool_call",
)?;
Ok(())
}

177
ecc2/src/session/daemon.rs Normal file
View File

@@ -0,0 +1,177 @@
use anyhow::Result;
use std::time::Duration;
use tokio::time;
use super::store::StateStore;
use super::SessionState;
use crate::config::Config;
/// Background daemon that monitors sessions, handles heartbeats,
/// and cleans up stale resources.
pub async fn run(db: StateStore, cfg: Config) -> Result<()> {
tracing::info!("ECC daemon started");
resume_crashed_sessions(&db)?;
let heartbeat_interval = Duration::from_secs(cfg.heartbeat_interval_secs);
let timeout = Duration::from_secs(cfg.session_timeout_secs);
loop {
if let Err(e) = check_sessions(&db, timeout) {
tracing::error!("Session check failed: {e}");
}
time::sleep(heartbeat_interval).await;
}
}
pub fn resume_crashed_sessions(db: &StateStore) -> Result<()> {
let failed_sessions = resume_crashed_sessions_with(db, pid_is_alive)?;
if failed_sessions > 0 {
tracing::warn!("Marked {failed_sessions} crashed sessions as failed during daemon startup");
}
Ok(())
}
fn resume_crashed_sessions_with<F>(db: &StateStore, is_pid_alive: F) -> Result<usize>
where
F: Fn(u32) -> bool,
{
let sessions = db.list_sessions()?;
let mut failed_sessions = 0;
for session in sessions {
if session.state != SessionState::Running {
continue;
}
let is_alive = session.pid.is_some_and(&is_pid_alive);
if is_alive {
continue;
}
tracing::warn!(
"Session {} was left running with stale pid {:?}; marking it failed",
session.id,
session.pid
);
db.update_state_and_pid(&session.id, &SessionState::Failed, None)?;
failed_sessions += 1;
}
Ok(failed_sessions)
}
fn check_sessions(db: &StateStore, timeout: Duration) -> Result<()> {
let sessions = db.list_sessions()?;
for session in sessions {
if session.state != SessionState::Running {
continue;
}
let elapsed = chrono::Utc::now()
.signed_duration_since(session.updated_at)
.to_std()
.unwrap_or(Duration::ZERO);
if elapsed > timeout {
tracing::warn!("Session {} timed out after {:?}", session.id, elapsed);
db.update_state_and_pid(&session.id, &SessionState::Failed, None)?;
}
}
Ok(())
}
#[cfg(unix)]
fn pid_is_alive(pid: u32) -> bool {
if pid == 0 {
return false;
}
// SAFETY: kill(pid, 0) probes process existence without delivering a signal.
let result = unsafe { libc::kill(pid as libc::pid_t, 0) };
if result == 0 {
return true;
}
matches!(
std::io::Error::last_os_error().raw_os_error(),
Some(code) if code == libc::EPERM
)
}
#[cfg(not(unix))]
fn pid_is_alive(_pid: u32) -> bool {
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::{Session, SessionMetrics, SessionState};
use std::path::PathBuf;
fn temp_db_path() -> PathBuf {
std::env::temp_dir().join(format!("ecc2-daemon-test-{}.db", uuid::Uuid::new_v4()))
}
fn sample_session(id: &str, state: SessionState, pid: Option<u32>) -> Session {
let now = chrono::Utc::now();
Session {
id: id.to_string(),
task: "Recover crashed worker".to_string(),
agent_type: "claude".to_string(),
state,
pid,
worktree: None,
created_at: now,
updated_at: now,
metrics: SessionMetrics::default(),
}
}
#[test]
fn resume_crashed_sessions_marks_dead_running_sessions_failed() -> Result<()> {
let path = temp_db_path();
let store = StateStore::open(&path)?;
store.insert_session(&sample_session(
"deadbeef",
SessionState::Running,
Some(4242),
))?;
resume_crashed_sessions_with(&store, |_| false)?;
let session = store
.get_session("deadbeef")?
.expect("session should still exist");
assert_eq!(session.state, SessionState::Failed);
assert_eq!(session.pid, None);
let _ = std::fs::remove_file(path);
Ok(())
}
#[test]
fn resume_crashed_sessions_keeps_live_running_sessions_running() -> Result<()> {
let path = temp_db_path();
let store = StateStore::open(&path)?;
store.insert_session(&sample_session(
"alive123",
SessionState::Running,
Some(7777),
))?;
resume_crashed_sessions_with(&store, |_| true)?;
let session = store
.get_session("alive123")?
.expect("session should still exist");
assert_eq!(session.state, SessionState::Running);
assert_eq!(session.pid, Some(7777));
let _ = std::fs::remove_file(path);
Ok(())
}
}

149
ecc2/src/session/manager.rs Normal file
View File

@@ -0,0 +1,149 @@
use anyhow::Result;
use std::fmt;
use super::store::StateStore;
use super::{Session, SessionMetrics, SessionState};
use crate::config::Config;
use crate::worktree;
pub async fn create_session(
db: &StateStore,
cfg: &Config,
task: &str,
agent_type: &str,
use_worktree: bool,
) -> Result<String> {
let id = uuid::Uuid::new_v4().to_string()[..8].to_string();
let now = chrono::Utc::now();
let wt = if use_worktree {
Some(worktree::create_for_session(&id, cfg)?)
} else {
None
};
let session = Session {
id: id.clone(),
task: task.to_string(),
agent_type: agent_type.to_string(),
state: SessionState::Pending,
pid: None,
worktree: wt,
created_at: now,
updated_at: now,
metrics: SessionMetrics::default(),
};
db.insert_session(&session)?;
Ok(id)
}
pub fn list_sessions(db: &StateStore) -> Result<Vec<Session>> {
db.list_sessions()
}
pub fn get_status(db: &StateStore, id: &str) -> Result<SessionStatus> {
let session = db
.get_session(id)?
.ok_or_else(|| anyhow::anyhow!("Session not found: {id}"))?;
Ok(SessionStatus(session))
}
pub async fn stop_session(db: &StateStore, id: &str) -> Result<()> {
let session = db
.get_session(id)?
.ok_or_else(|| anyhow::anyhow!("Session not found: {id}"))?;
db.update_state_and_pid(&session.id, &SessionState::Stopped, None)?;
Ok(())
}
pub async fn resume_session(db: &StateStore, id: &str) -> Result<String> {
let session = db
.get_session(id)?
.ok_or_else(|| anyhow::anyhow!("Session not found: {id}"))?;
if session.state == SessionState::Completed {
anyhow::bail!("Completed sessions cannot be resumed: {}", session.id);
}
if session.state == SessionState::Running {
anyhow::bail!("Session is already running: {}", session.id);
}
db.update_state_and_pid(&session.id, &SessionState::Pending, None)?;
Ok(session.id)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::store::StateStore;
use std::path::PathBuf;
fn temp_db_path() -> PathBuf {
std::env::temp_dir().join(format!("ecc2-manager-test-{}.db", uuid::Uuid::new_v4()))
}
fn sample_session(id: &str, state: SessionState, pid: Option<u32>) -> Session {
let now = chrono::Utc::now();
Session {
id: id.to_string(),
task: "Resume previous task".to_string(),
agent_type: "claude".to_string(),
state,
pid,
worktree: None,
created_at: now,
updated_at: now,
metrics: SessionMetrics::default(),
}
}
#[tokio::test]
async fn resume_session_requeues_failed_session() -> Result<()> {
let path = temp_db_path();
let store = StateStore::open(&path)?;
store.insert_session(&sample_session(
"deadbeef",
SessionState::Failed,
Some(31337),
))?;
let resumed_id = resume_session(&store, "deadbeef").await?;
let resumed = store
.get_session(&resumed_id)?
.expect("resumed session should exist");
assert_eq!(resumed.state, SessionState::Pending);
assert_eq!(resumed.pid, None);
let _ = std::fs::remove_file(path);
Ok(())
}
}
pub struct SessionStatus(Session);
impl fmt::Display for SessionStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = &self.0;
writeln!(f, "Session: {}", s.id)?;
writeln!(f, "Task: {}", s.task)?;
writeln!(f, "Agent: {}", s.agent_type)?;
writeln!(f, "State: {}", s.state)?;
if let Some(pid) = s.pid {
writeln!(f, "PID: {pid}")?;
}
if let Some(ref wt) = s.worktree {
writeln!(f, "Branch: {}", wt.branch)?;
writeln!(f, "Worktree: {}", wt.path.display())?;
}
writeln!(f, "Tokens: {}", s.metrics.tokens_used)?;
writeln!(f, "Tools: {}", s.metrics.tool_calls)?;
writeln!(f, "Files: {}", s.metrics.files_changed)?;
writeln!(f, "Cost: ${:.4}", s.metrics.cost_usd)?;
writeln!(f, "Created: {}", s.created_at)?;
write!(f, "Updated: {}", s.updated_at)
}
}

60
ecc2/src/session/mod.rs Normal file
View File

@@ -0,0 +1,60 @@
pub mod daemon;
pub mod manager;
pub mod store;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
pub id: String,
pub task: String,
pub agent_type: String,
pub state: SessionState,
pub pid: Option<u32>,
pub worktree: Option<WorktreeInfo>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub metrics: SessionMetrics,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum SessionState {
Pending,
Running,
Idle,
Completed,
Failed,
Stopped,
}
impl fmt::Display for SessionState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SessionState::Pending => write!(f, "pending"),
SessionState::Running => write!(f, "running"),
SessionState::Idle => write!(f, "idle"),
SessionState::Completed => write!(f, "completed"),
SessionState::Failed => write!(f, "failed"),
SessionState::Stopped => write!(f, "stopped"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorktreeInfo {
pub path: PathBuf,
pub branch: String,
pub base_branch: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SessionMetrics {
pub tokens_used: u64,
pub tool_calls: u64,
pub files_changed: u32,
pub duration_secs: u64,
pub cost_usd: f64,
}

321
ecc2/src/session/store.rs Normal file
View File

@@ -0,0 +1,321 @@
use anyhow::Result;
use rusqlite::Connection;
use std::path::Path;
use super::{Session, SessionMetrics, SessionState};
pub struct StateStore {
conn: Connection,
}
impl StateStore {
pub fn open(path: &Path) -> Result<Self> {
let conn = Connection::open(path)?;
let store = Self { conn };
store.init_schema()?;
Ok(store)
}
fn init_schema(&self) -> Result<()> {
self.conn.execute_batch(
"
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
task TEXT NOT NULL,
agent_type TEXT NOT NULL,
state TEXT NOT NULL DEFAULT 'pending',
pid INTEGER,
worktree_path TEXT,
worktree_branch TEXT,
worktree_base TEXT,
tokens_used INTEGER DEFAULT 0,
tool_calls INTEGER DEFAULT 0,
files_changed INTEGER DEFAULT 0,
duration_secs INTEGER DEFAULT 0,
cost_usd REAL DEFAULT 0.0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS tool_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL REFERENCES sessions(id),
tool_name TEXT NOT NULL,
input_summary TEXT,
output_summary TEXT,
duration_ms INTEGER,
risk_score REAL DEFAULT 0.0,
timestamp TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_session TEXT NOT NULL,
to_session TEXT NOT NULL,
content TEXT NOT NULL,
msg_type TEXT NOT NULL DEFAULT 'info',
read INTEGER DEFAULT 0,
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);
",
)?;
self.ensure_sessions_pid_column()?;
Ok(())
}
fn ensure_sessions_pid_column(&self) -> Result<()> {
let mut stmt = self.conn.prepare("PRAGMA table_info(sessions)")?;
let mut rows = stmt.query([])?;
while let Some(row) = rows.next()? {
let column_name: String = row.get(1)?;
if column_name == "pid" {
return Ok(());
}
}
self.conn
.execute("ALTER TABLE sessions ADD COLUMN pid INTEGER", [])?;
Ok(())
}
pub fn insert_session(&self, session: &Session) -> Result<()> {
self.conn.execute(
"INSERT INTO sessions (id, task, agent_type, state, pid, worktree_path, worktree_branch, worktree_base, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
rusqlite::params![
session.id,
session.task,
session.agent_type,
session.state.to_string(),
session.pid.map(i64::from),
session.worktree.as_ref().map(|w| w.path.to_string_lossy().to_string()),
session.worktree.as_ref().map(|w| w.branch.clone()),
session.worktree.as_ref().map(|w| w.base_branch.clone()),
session.created_at.to_rfc3339(),
session.updated_at.to_rfc3339(),
],
)?;
Ok(())
}
pub fn update_state_and_pid(
&self,
session_id: &str,
state: &SessionState,
pid: Option<u32>,
) -> Result<()> {
self.conn.execute(
"UPDATE sessions SET state = ?1, pid = ?2, updated_at = ?3 WHERE id = ?4",
rusqlite::params![
state.to_string(),
pid.map(i64::from),
chrono::Utc::now().to_rfc3339(),
session_id,
],
)?;
Ok(())
}
pub fn update_state(&self, session_id: &str, state: &SessionState) -> Result<()> {
self.conn.execute(
"UPDATE sessions SET state = ?1, updated_at = ?2 WHERE id = ?3",
rusqlite::params![
state.to_string(),
chrono::Utc::now().to_rfc3339(),
session_id,
],
)?;
Ok(())
}
pub fn update_metrics(&self, session_id: &str, metrics: &SessionMetrics) -> Result<()> {
self.conn.execute(
"UPDATE sessions SET tokens_used = ?1, tool_calls = ?2, files_changed = ?3, duration_secs = ?4, cost_usd = ?5, updated_at = ?6 WHERE id = ?7",
rusqlite::params![
metrics.tokens_used,
metrics.tool_calls,
metrics.files_changed,
metrics.duration_secs,
metrics.cost_usd,
chrono::Utc::now().to_rfc3339(),
session_id,
],
)?;
Ok(())
}
pub fn list_sessions(&self) -> Result<Vec<Session>> {
let mut stmt = self.conn.prepare(
"SELECT id, task, agent_type, state, pid, worktree_path, worktree_branch, worktree_base,
tokens_used, tool_calls, files_changed, duration_secs, cost_usd,
created_at, updated_at
FROM sessions ORDER BY updated_at DESC",
)?;
let sessions = stmt
.query_map([], |row| {
let state_str: String = row.get(3)?;
let state = match state_str.as_str() {
"running" => SessionState::Running,
"idle" => SessionState::Idle,
"completed" => SessionState::Completed,
"failed" => SessionState::Failed,
"stopped" => SessionState::Stopped,
_ => SessionState::Pending,
};
let pid = row
.get::<_, Option<i64>>(4)?
.and_then(|value| u32::try_from(value).ok());
let worktree_path: Option<String> = row.get(5)?;
let worktree = worktree_path.map(|p| super::WorktreeInfo {
path: std::path::PathBuf::from(p),
branch: row.get::<_, String>(6).unwrap_or_default(),
base_branch: row.get::<_, String>(7).unwrap_or_default(),
});
let created_str: String = row.get(13)?;
let updated_str: String = row.get(14)?;
Ok(Session {
id: row.get(0)?,
task: row.get(1)?,
agent_type: row.get(2)?,
state,
pid,
worktree,
created_at: chrono::DateTime::parse_from_rfc3339(&created_str)
.unwrap_or_default()
.with_timezone(&chrono::Utc),
updated_at: chrono::DateTime::parse_from_rfc3339(&updated_str)
.unwrap_or_default()
.with_timezone(&chrono::Utc),
metrics: SessionMetrics {
tokens_used: row.get(8)?,
tool_calls: row.get(9)?,
files_changed: row.get(10)?,
duration_secs: row.get(11)?,
cost_usd: row.get(12)?,
},
})
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(sessions)
}
pub fn get_session(&self, id: &str) -> Result<Option<Session>> {
let sessions = self.list_sessions()?;
Ok(sessions
.into_iter()
.find(|s| s.id == id || s.id.starts_with(id)))
}
pub fn send_message(&self, from: &str, to: &str, content: &str, msg_type: &str) -> Result<()> {
self.conn.execute(
"INSERT INTO messages (from_session, to_session, content, msg_type, timestamp)
VALUES (?1, ?2, ?3, ?4, ?5)",
rusqlite::params![from, to, content, msg_type, chrono::Utc::now().to_rfc3339()],
)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::{Session, SessionMetrics, SessionState};
use std::path::PathBuf;
fn temp_db_path() -> PathBuf {
std::env::temp_dir().join(format!("ecc2-store-test-{}.db", uuid::Uuid::new_v4()))
}
fn sample_session(id: &str, state: SessionState, pid: Option<u32>) -> Session {
let now = chrono::Utc::now();
Session {
id: id.to_string(),
task: "Investigate crash".to_string(),
agent_type: "claude".to_string(),
state,
pid,
worktree: None,
created_at: now,
updated_at: now,
metrics: SessionMetrics::default(),
}
}
#[test]
fn open_migrates_existing_sessions_table_with_pid_column() -> Result<()> {
let path = temp_db_path();
let conn = Connection::open(&path)?;
conn.execute_batch(
"
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
task TEXT NOT NULL,
agent_type TEXT NOT NULL,
state TEXT NOT NULL DEFAULT 'pending',
worktree_path TEXT,
worktree_branch TEXT,
worktree_base TEXT,
tokens_used INTEGER DEFAULT 0,
tool_calls INTEGER DEFAULT 0,
files_changed INTEGER DEFAULT 0,
duration_secs INTEGER DEFAULT 0,
cost_usd REAL DEFAULT 0.0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
",
)?;
conn.execute(
"INSERT INTO sessions (id, task, agent_type, state, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
rusqlite::params![
"legacy",
"Recover state",
"claude",
"running",
chrono::Utc::now().to_rfc3339(),
chrono::Utc::now().to_rfc3339(),
],
)?;
drop(conn);
let store = StateStore::open(&path)?;
let session = store
.get_session("legacy")?
.expect("legacy session should load");
assert_eq!(session.pid, None);
let _ = std::fs::remove_file(path);
Ok(())
}
#[test]
fn insert_session_persists_pid() -> Result<()> {
let path = temp_db_path();
let store = StateStore::open(&path)?;
let session = sample_session("abc12345", SessionState::Running, Some(4242));
store.insert_session(&session)?;
let loaded = store
.get_session("abc12345")?
.expect("session should be persisted");
assert_eq!(loaded.pid, Some(4242));
assert_eq!(loaded.state, SessionState::Running);
let _ = std::fs::remove_file(path);
Ok(())
}
}

52
ecc2/src/tui/app.rs Normal file
View File

@@ -0,0 +1,52 @@
use anyhow::Result;
use crossterm::{
event::{self, Event, KeyCode, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::prelude::*;
use std::io;
use std::time::Duration;
use super::dashboard::Dashboard;
use crate::config::Config;
use crate::session::store::StateStore;
pub async fn run(db: StateStore, cfg: Config) -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut dashboard = Dashboard::new(db, cfg);
loop {
terminal.draw(|frame| dashboard.render(frame))?;
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
match (key.modifiers, key.code) {
(KeyModifiers::CONTROL, KeyCode::Char('c')) => break,
(_, KeyCode::Char('q')) => break,
(_, KeyCode::Tab) => dashboard.next_pane(),
(KeyModifiers::SHIFT, KeyCode::BackTab) => dashboard.prev_pane(),
(_, KeyCode::Char('j')) | (_, KeyCode::Down) => dashboard.scroll_down(),
(_, KeyCode::Char('k')) | (_, KeyCode::Up) => dashboard.scroll_up(),
(_, KeyCode::Char('n')) => dashboard.new_session(),
(_, KeyCode::Char('s')) => dashboard.stop_selected(),
(_, KeyCode::Char('r')) => dashboard.refresh(),
(_, KeyCode::Char('?')) => dashboard.toggle_help(),
_ => {}
}
}
}
dashboard.tick().await;
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
Ok(())
}

293
ecc2/src/tui/dashboard.rs Normal file
View File

@@ -0,0 +1,293 @@
use ratatui::{
prelude::*,
widgets::{Block, Borders, List, ListItem, Paragraph, Tabs},
};
use crate::config::Config;
use crate::session::store::StateStore;
use crate::session::{Session, SessionState};
pub struct Dashboard {
db: StateStore,
cfg: Config,
sessions: Vec<Session>,
selected_pane: Pane,
selected_session: usize,
show_help: bool,
scroll_offset: usize,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Pane {
Sessions,
Output,
Metrics,
}
impl Dashboard {
pub fn new(db: StateStore, cfg: Config) -> Self {
let sessions = db.list_sessions().unwrap_or_default();
Self {
db,
cfg,
sessions,
selected_pane: Pane::Sessions,
selected_session: 0,
show_help: false,
scroll_offset: 0,
}
}
pub fn render(&self, frame: &mut Frame) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // Header
Constraint::Min(10), // Main content
Constraint::Length(3), // Status bar
])
.split(frame.area());
self.render_header(frame, chunks[0]);
if self.show_help {
self.render_help(frame, chunks[1]);
} else {
let main_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(35), // Session list
Constraint::Percentage(65), // Output/details
])
.split(chunks[1]);
self.render_sessions(frame, main_chunks[0]);
let right_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(70), // Output
Constraint::Percentage(30), // Metrics
])
.split(main_chunks[1]);
self.render_output(frame, right_chunks[0]);
self.render_metrics(frame, right_chunks[1]);
}
self.render_status_bar(frame, chunks[2]);
}
fn render_header(&self, frame: &mut Frame, area: Rect) {
let running = self
.sessions
.iter()
.filter(|s| s.state == SessionState::Running)
.count();
let total = self.sessions.len();
let title = format!(" ECC 2.0 | {running} running / {total} total ");
let tabs = Tabs::new(vec!["Sessions", "Output", "Metrics"])
.block(Block::default().borders(Borders::ALL).title(title))
.select(match self.selected_pane {
Pane::Sessions => 0,
Pane::Output => 1,
Pane::Metrics => 2,
})
.highlight_style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
);
frame.render_widget(tabs, area);
}
fn render_sessions(&self, frame: &mut Frame, area: Rect) {
let items: Vec<ListItem> = self
.sessions
.iter()
.enumerate()
.map(|(i, s)| {
let state_icon = match s.state {
SessionState::Running => "",
SessionState::Idle => "",
SessionState::Completed => "",
SessionState::Failed => "",
SessionState::Stopped => "",
SessionState::Pending => "",
};
let style = if i == self.selected_session {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
} else {
Style::default()
};
let text = format!(
"{state_icon} {} [{}] {}",
&s.id[..8.min(s.id.len())],
s.agent_type,
s.task
);
ListItem::new(text).style(style)
})
.collect();
let border_style = if self.selected_pane == Pane::Sessions {
Style::default().fg(Color::Cyan)
} else {
Style::default()
};
let list = List::new(items).block(
Block::default()
.borders(Borders::ALL)
.title(" Sessions ")
.border_style(border_style),
);
frame.render_widget(list, area);
}
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()
};
let border_style = if self.selected_pane == Pane::Output {
Style::default().fg(Color::Cyan)
} else {
Style::default()
};
let paragraph = Paragraph::new(content).block(
Block::default()
.borders(Borders::ALL)
.title(" Output ")
.border_style(border_style),
);
frame.render_widget(paragraph, area);
}
fn render_metrics(&self, frame: &mut Frame, area: Rect) {
let content = if let Some(session) = self.sessions.get(self.selected_session) {
let m = &session.metrics;
format!(
"Tokens: {} | Tools: {} | Files: {} | Cost: ${:.4} | Duration: {}s",
m.tokens_used, m.tool_calls, m.files_changed, m.cost_usd, m.duration_secs
)
} else {
"No metrics available".to_string()
};
let border_style = if self.selected_pane == Pane::Metrics {
Style::default().fg(Color::Cyan)
} else {
Style::default()
};
let paragraph = Paragraph::new(content).block(
Block::default()
.borders(Borders::ALL)
.title(" Metrics ")
.border_style(border_style),
);
frame.render_widget(paragraph, area);
}
fn render_status_bar(&self, frame: &mut Frame, area: Rect) {
let text = " [n]ew session [s]top [Tab] switch pane [j/k] scroll [?] help [q]uit ";
let paragraph = Paragraph::new(text)
.style(Style::default().fg(Color::DarkGray))
.block(Block::default().borders(Borders::ALL));
frame.render_widget(paragraph, area);
}
fn render_help(&self, frame: &mut Frame, area: Rect) {
let help = vec![
"Keyboard Shortcuts:",
"",
" n New session",
" s Stop selected session",
" Tab Next pane",
" S-Tab Previous pane",
" j/↓ Scroll down",
" k/↑ Scroll up",
" r Refresh",
" ? Toggle help",
" q/C-c Quit",
];
let paragraph = Paragraph::new(help.join("\n")).block(
Block::default()
.borders(Borders::ALL)
.title(" Help ")
.border_style(Style::default().fg(Color::Yellow)),
);
frame.render_widget(paragraph, area);
}
pub fn next_pane(&mut self) {
self.selected_pane = match self.selected_pane {
Pane::Sessions => Pane::Output,
Pane::Output => Pane::Metrics,
Pane::Metrics => Pane::Sessions,
};
}
pub fn prev_pane(&mut self) {
self.selected_pane = match self.selected_pane {
Pane::Sessions => Pane::Metrics,
Pane::Output => Pane::Sessions,
Pane::Metrics => Pane::Output,
};
}
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);
} 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);
} else {
self.scroll_offset = self.scroll_offset.saturating_sub(1);
}
}
pub fn new_session(&mut self) {
// TODO: Open a dialog to create a new session
tracing::info!("New session dialog requested");
}
pub fn stop_selected(&mut self) {
if let Some(session) = self.sessions.get(self.selected_session) {
let _ = self
.db
.update_state_and_pid(&session.id, &SessionState::Stopped, None);
self.refresh();
}
}
pub fn refresh(&mut self) {
self.sessions = self.db.list_sessions().unwrap_or_default();
}
pub fn toggle_help(&mut self) {
self.show_help = !self.show_help;
}
pub async fn tick(&mut self) {
// Periodic refresh every few ticks
self.sessions = self.db.list_sessions().unwrap_or_default();
}
}

3
ecc2/src/tui/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod app;
mod dashboard;
mod widgets;

6
ecc2/src/tui/widgets.rs Normal file
View File

@@ -0,0 +1,6 @@
// Custom TUI widgets for ECC 2.0
// TODO: Implement custom widgets:
// - TokenMeter: visual token usage bar with budget threshold
// - DiffViewer: side-by-side syntax-highlighted diff display
// - ProgressTimeline: session timeline with tool call markers
// - AgentTree: hierarchical view of parent/child agent sessions

80
ecc2/src/worktree/mod.rs Normal file
View File

@@ -0,0 +1,80 @@
use anyhow::{Context, Result};
use std::path::PathBuf;
use std::process::Command;
use crate::config::Config;
use crate::session::WorktreeInfo;
/// Create a new git worktree for an agent session.
pub fn create_for_session(session_id: &str, cfg: &Config) -> Result<WorktreeInfo> {
let branch = format!("ecc/{session_id}");
let path = cfg.worktree_root.join(session_id);
// Get current branch as base
let base = get_current_branch()?;
std::fs::create_dir_all(&cfg.worktree_root)
.context("Failed to create worktree root directory")?;
let output = Command::new("git")
.args(["worktree", "add", "-b", &branch])
.arg(&path)
.arg("HEAD")
.output()
.context("Failed to run git worktree add")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git worktree add failed: {stderr}");
}
tracing::info!("Created worktree at {} on branch {}", path.display(), branch);
Ok(WorktreeInfo {
path,
branch,
base_branch: base,
})
}
/// Remove a worktree and its branch.
pub fn remove(path: &PathBuf) -> Result<()> {
let output = Command::new("git")
.args(["worktree", "remove", "--force"])
.arg(path)
.output()
.context("Failed to remove worktree")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
tracing::warn!("Worktree removal warning: {stderr}");
}
Ok(())
}
/// List all active worktrees.
pub fn list() -> Result<Vec<String>> {
let output = Command::new("git")
.args(["worktree", "list", "--porcelain"])
.output()
.context("Failed to list worktrees")?;
let stdout = String::from_utf8_lossy(&output.stdout);
let worktrees: Vec<String> = stdout
.lines()
.filter(|l| l.starts_with("worktree "))
.map(|l| l.trim_start_matches("worktree ").to_string())
.collect();
Ok(worktrees)
}
fn get_current_branch() -> Result<String> {
let output = Command::new("git")
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.output()
.context("Failed to get current branch")?;
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}