Compare commits

..

46 Commits

Author SHA1 Message Date
ecc-tools[bot]
9aac8d3904 feat: add everything-claude-code ECC bundle (.claude/commands/add-or-update-skill.md) 2026-03-24 10:44:24 +00:00
ecc-tools[bot]
3e8efe77c8 feat: add everything-claude-code ECC bundle (.claude/commands/feature-development.md) 2026-03-24 10:44:23 +00:00
ecc-tools[bot]
48f27e6919 feat: add everything-claude-code ECC bundle (.claude/commands/database-migration.md) 2026-03-24 10:44:22 +00:00
ecc-tools[bot]
f0ea102ec7 feat: add everything-claude-code ECC bundle (.claude/enterprise/controls.md) 2026-03-24 10:44:21 +00:00
ecc-tools[bot]
65cb56ced6 feat: add everything-claude-code ECC bundle (.claude/team/everything-claude-code-team-config.json) 2026-03-24 10:44:20 +00:00
ecc-tools[bot]
3e4a74e580 feat: add everything-claude-code ECC bundle (.claude/research/everything-claude-code-research-playbook.md) 2026-03-24 10:44:19 +00:00
ecc-tools[bot]
1b64d43b8d feat: add everything-claude-code ECC bundle (.claude/rules/everything-claude-code-guardrails.md) 2026-03-24 10:44:18 +00:00
ecc-tools[bot]
01bdcc9f5e feat: add everything-claude-code ECC bundle (.codex/agents/docs-researcher.toml) 2026-03-24 10:44:17 +00:00
ecc-tools[bot]
ecbe939a21 feat: add everything-claude-code ECC bundle (.codex/agents/reviewer.toml) 2026-03-24 10:44:17 +00:00
ecc-tools[bot]
af45edc199 feat: add everything-claude-code ECC bundle (.codex/agents/explorer.toml) 2026-03-24 10:44:16 +00:00
ecc-tools[bot]
232b94873b feat: add everything-claude-code ECC bundle (.claude/identity.json) 2026-03-24 10:44:15 +00:00
ecc-tools[bot]
1eca64a933 feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/agents/openai.yaml) 2026-03-24 10:44:14 +00:00
ecc-tools[bot]
b1c1805671 feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/SKILL.md) 2026-03-24 10:44:13 +00:00
ecc-tools[bot]
8d7908ee8c feat: add everything-claude-code ECC bundle (.claude/skills/everything-claude-code/SKILL.md) 2026-03-24 10:44:12 +00:00
ecc-tools[bot]
c547b77f47 feat: add everything-claude-code ECC bundle (.claude/ecc-tools.json) 2026-03-24 10:44:11 +00:00
ecc-tools[bot]
f8d5e9e54c feat: add everything-claude-code ECC bundle (.claude/commands/add-or-update-skill.md) 2026-03-24 10:43:36 +00:00
ecc-tools[bot]
30c6a93d62 feat: add everything-claude-code ECC bundle (.claude/commands/feature-development.md) 2026-03-24 10:43:35 +00:00
ecc-tools[bot]
b05e272549 feat: add everything-claude-code ECC bundle (.claude/commands/database-migration.md) 2026-03-24 10:43:35 +00:00
ecc-tools[bot]
ff9ded5b92 feat: add everything-claude-code ECC bundle (.claude/enterprise/controls.md) 2026-03-24 10:43:34 +00:00
ecc-tools[bot]
f448eeb07b feat: add everything-claude-code ECC bundle (.claude/team/everything-claude-code-team-config.json) 2026-03-24 10:43:33 +00:00
ecc-tools[bot]
ff0ac80fad feat: add everything-claude-code ECC bundle (.claude/research/everything-claude-code-research-playbook.md) 2026-03-24 10:43:32 +00:00
ecc-tools[bot]
ccd43b8dc2 feat: add everything-claude-code ECC bundle (.claude/rules/everything-claude-code-guardrails.md) 2026-03-24 10:43:31 +00:00
ecc-tools[bot]
4e05da7d18 feat: add everything-claude-code ECC bundle (.codex/agents/docs-researcher.toml) 2026-03-24 10:43:30 +00:00
ecc-tools[bot]
161e08890e feat: add everything-claude-code ECC bundle (.codex/agents/reviewer.toml) 2026-03-24 10:43:30 +00:00
ecc-tools[bot]
792f8ae002 feat: add everything-claude-code ECC bundle (.codex/agents/explorer.toml) 2026-03-24 10:43:29 +00:00
ecc-tools[bot]
238d5478b7 feat: add everything-claude-code ECC bundle (.claude/identity.json) 2026-03-24 10:43:27 +00:00
ecc-tools[bot]
a8f26f47fd feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/agents/openai.yaml) 2026-03-24 10:43:27 +00:00
ecc-tools[bot]
249f4f81ab feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/SKILL.md) 2026-03-24 10:43:26 +00:00
ecc-tools[bot]
c91a13693c feat: add everything-claude-code ECC bundle (.claude/skills/everything-claude-code/SKILL.md) 2026-03-24 10:43:25 +00:00
ecc-tools[bot]
9680dfb8ee feat: add everything-claude-code ECC bundle (.claude/ecc-tools.json) 2026-03-24 10:43:24 +00:00
ecc-tools[bot]
24386fe30d feat: add everything-claude-code ECC bundle (.claude/commands/add-or-update-skill-documentation.md) 2026-03-24 10:42:00 +00:00
ecc-tools[bot]
0468c2159d feat: add everything-claude-code ECC bundle (.claude/commands/feature-development.md) 2026-03-24 10:41:59 +00:00
ecc-tools[bot]
c13e1d45e3 feat: add everything-claude-code ECC bundle (.claude/commands/database-migration.md) 2026-03-24 10:41:58 +00:00
ecc-tools[bot]
fe4fecc27d feat: add everything-claude-code ECC bundle (.claude/enterprise/controls.md) 2026-03-24 10:41:57 +00:00
ecc-tools[bot]
a8b29d96e9 feat: add everything-claude-code ECC bundle (.claude/team/everything-claude-code-team-config.json) 2026-03-24 10:41:56 +00:00
ecc-tools[bot]
9207edec71 feat: add everything-claude-code ECC bundle (.claude/research/everything-claude-code-research-playbook.md) 2026-03-24 10:41:55 +00:00
ecc-tools[bot]
549971e234 feat: add everything-claude-code ECC bundle (.claude/rules/everything-claude-code-guardrails.md) 2026-03-24 10:41:54 +00:00
ecc-tools[bot]
2a37a8197c feat: add everything-claude-code ECC bundle (.codex/agents/docs-researcher.toml) 2026-03-24 10:41:53 +00:00
ecc-tools[bot]
781dbfcbfe feat: add everything-claude-code ECC bundle (.codex/agents/reviewer.toml) 2026-03-24 10:41:53 +00:00
ecc-tools[bot]
796f0e4b56 feat: add everything-claude-code ECC bundle (.codex/agents/explorer.toml) 2026-03-24 10:41:52 +00:00
ecc-tools[bot]
124969f3c4 feat: add everything-claude-code ECC bundle (.claude/identity.json) 2026-03-24 10:41:51 +00:00
ecc-tools[bot]
1ec027cb9a feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/agents/openai.yaml) 2026-03-24 10:41:50 +00:00
ecc-tools[bot]
c688bde7a8 feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/SKILL.md) 2026-03-24 10:41:49 +00:00
ecc-tools[bot]
3ae5a975ba feat: add everything-claude-code ECC bundle (.claude/skills/everything-claude-code/SKILL.md) 2026-03-24 10:41:48 +00:00
ecc-tools[bot]
1cae8895c7 feat: add everything-claude-code ECC bundle (.claude/ecc-tools.json) 2026-03-24 10:41:47 +00:00
Affaan Mustafa
8b34f72df3 feat(ecc2): add tool call logging and history 2026-03-24 03:39:53 -07:00
19 changed files with 654 additions and 792 deletions

View File

@@ -228,77 +228,78 @@ fix: bump plugin.json and marketplace.json to v1.9.0
Add Turkish (tr) docs and update README (#744)
```
### Add Or Update Ecc Command Doc
### Add Or Update Skill
Adds or updates documentation for an ECC command, typically as a Markdown file under .claude/commands.
Adds or updates a skill, including its documentation and configuration.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update .claude/commands/add-or-update-skill.md
2. Create or update .claude/skills/<skill-name>/SKILL.md
3. Optionally update .agents/skills/<skill-name>/SKILL.md
4. Optionally update .agents/skills/<skill-name>/agents/*.yaml
**Files typically involved**:
- `.claude/commands/add-or-update-skill.md`
- `.claude/skills/*/SKILL.md`
- `.agents/skills/*/SKILL.md`
- `.agents/skills/*/agents/*.yaml`
**Example commit sequence**:
```
Create or update .claude/commands/add-or-update-skill.md
Create or update .claude/skills/<skill-name>/SKILL.md
Optionally update .agents/skills/<skill-name>/SKILL.md
Optionally update .agents/skills/<skill-name>/agents/*.yaml
```
### Add Or Update Command Documentation
Adds or updates documentation for a CLI command or workflow.
**Frequency**: ~3 times per month
**Steps**:
1. Create or update a Markdown file in .claude/commands/ describing the command.
2. Optionally, update related documentation elsewhere.
1. Create or update .claude/commands/<command-name>.md
**Files typically involved**:
- `.claude/commands/*.md`
**Example commit sequence**:
```
Create or update a Markdown file in .claude/commands/ describing the command.
Optionally, update related documentation elsewhere.
Create or update .claude/commands/<command-name>.md
```
### Add Or Update Skill Doc
### Add Or Update Database Migration Patterns
Adds or updates documentation for a skill, typically as SKILL.md under a skill directory.
**Frequency**: ~3 times per month
**Steps**:
1. Create or update SKILL.md in the relevant skill directory (e.g., skills/skill-name/SKILL.md).
2. Optionally, update related documentation or diagrams.
**Files typically involved**:
- `skills/*/SKILL.md`
- `.agents/skills/*/SKILL.md`
- `.claude/skills/*/SKILL.md`
**Example commit sequence**:
```
Create or update SKILL.md in the relevant skill directory (e.g., skills/skill-name/SKILL.md).
Optionally, update related documentation or diagrams.
```
### Add Or Update Localization Docs
Adds or updates localized documentation for agents, commands, skills, and guides in a new or existing language.
Adds or updates database migration patterns or documentation.
**Frequency**: ~2 times per month
**Steps**:
1. Add or update multiple Markdown files under docs/<lang>/ for agents, commands, skills, rules, and guides.
2. Update README.md to reflect supported languages.
1. Create or update .claude/commands/database-migration.md
2. Create or update skills/database-migrations/SKILL.md
**Files typically involved**:
- `docs/*/README.md`
- `docs/*/agents/*.md`
- `docs/*/commands/*.md`
- `docs/*/skills/*/SKILL.md`
- `docs/*/rules/**/*.md`
- `.claude/commands/database-migration.md`
- `skills/database-migrations/SKILL.md`
**Example commit sequence**:
```
Add or update multiple Markdown files under docs/<lang>/ for agents, commands, skills, rules, and guides.
Update README.md to reflect supported languages.
Create or update .claude/commands/database-migration.md
Create or update skills/database-migrations/SKILL.md
```
### Add Or Update Team Or Identity Config
Adds or updates team configuration or identity files for ECC.
Adds or updates team configuration or identity files.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update .claude/team/everything-claude-code-team-config.json or .claude/identity.json.
1. Create or update .claude/team/everything-claude-code-team-config.json
2. Create or update .claude/identity.json
**Files typically involved**:
- `.claude/team/everything-claude-code-team-config.json`
@@ -306,42 +307,79 @@ Adds or updates team configuration or identity files for ECC.
**Example commit sequence**:
```
Create or update .claude/team/everything-claude-code-team-config.json or .claude/identity.json.
Create or update .claude/team/everything-claude-code-team-config.json
Create or update .claude/identity.json
```
### Add Or Update Ecc Tools Config
### Add Or Update Guardrails Or Controls
Adds or updates the ECC tools configuration file.
Adds or updates project guardrails, rules, or enterprise controls.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update .claude/ecc-tools.json.
1. Create or update .claude/rules/everything-claude-code-guardrails.md
2. Create or update .claude/enterprise/controls.md
**Files typically involved**:
- `.claude/rules/everything-claude-code-guardrails.md`
- `.claude/enterprise/controls.md`
**Example commit sequence**:
```
Create or update .claude/rules/everything-claude-code-guardrails.md
Create or update .claude/enterprise/controls.md
```
### Add Or Update Agent Config
Adds or updates agent configuration TOML files.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update .codex/agents/*.toml
**Files typically involved**:
- `.codex/agents/*.toml`
**Example commit sequence**:
```
Create or update .codex/agents/*.toml
```
### Add Or Update Research Playbook
Adds or updates research playbooks or process documentation.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update .claude/research/everything-claude-code-research-playbook.md
**Files typically involved**:
- `.claude/research/everything-claude-code-research-playbook.md`
**Example commit sequence**:
```
Create or update .claude/research/everything-claude-code-research-playbook.md
```
### Add Or Update Ecc Tools Config
Adds or updates ECC tools configuration.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update .claude/ecc-tools.json
**Files typically involved**:
- `.claude/ecc-tools.json`
**Example commit sequence**:
```
Create or update .claude/ecc-tools.json.
```
### Add Or Update Agent Config
Adds or updates agent configuration TOML files for Codex or ECC agents.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update .codex/agents/*.toml or .agents/skills/*/agents/*.yaml.
**Files typically involved**:
- `.codex/agents/*.toml`
- `.agents/skills/*/agents/*.yaml`
**Example commit sequence**:
```
Create or update .codex/agents/*.toml or .agents/skills/*/agents/*.yaml.
Create or update .claude/ecc-tools.json
```

View File

@@ -1,34 +0,0 @@
---
name: add-or-update-ecc-command-doc
description: Workflow command scaffold for add-or-update-ecc-command-doc in everything-claude-code.
allowed_tools: ["Bash", "Read", "Write", "Grep", "Glob"]
---
# /add-or-update-ecc-command-doc
Use this workflow when working on **add-or-update-ecc-command-doc** in `everything-claude-code`.
## Goal
Adds or updates documentation for an ECC command, typically as a Markdown file under .claude/commands.
## Common Files
- `.claude/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 in .claude/commands/ describing the command.
- Optionally, update related documentation elsewhere.
## Notes
- Treat this as a scaffold, not a hard-coded script.
- Update the command if the workflow evolves materially.

View File

@@ -10,14 +10,12 @@ Use this workflow when working on **add-or-update-skill-documentation** in `ever
## Goal
Adds a new skill or updates documentation for an existing skill. Typically involves creating or modifying SKILL.md files under skills/ or docs/xx/skills/ directories.
Adds a new skill or updates existing skill documentation, typically in SKILL.md under skills/<skill-name>/ or docs/<lang>/skills/<skill-name>/SKILL.md.
## Common Files
- `skills/*/SKILL.md`
- `docs/*/skills/*/SKILL.md`
- `AGENTS.md`
- `README.md`
## Suggested Sequence
@@ -28,9 +26,9 @@ Adds a new skill or updates documentation for an existing skill. Typically invol
## Typical Commit Signals
- Create or update SKILL.md under skills/<skill-name>/ or docs/<lang>/skills/<skill-name>/
- Optionally update AGENTS.md or README.md to reflect new skill count or catalog
- Commit with message referencing the skill and a summary of changes
- Create or update SKILL.md in the appropriate skills/<skill-name>/ directory.
- Optionally update language-localized documentation under docs/<lang>/skills/<skill-name>/SKILL.md.
- Commit with a message referencing the skill and summary of the change.
## Notes

View File

@@ -10,13 +10,14 @@ Use this workflow when working on **add-or-update-skill** in `everything-claude-
## Goal
Adds a new skill or updates an existing skill, including documentation and configuration.
Adds or updates a skill, including its documentation and configuration.
## Common Files
- `skills/*/SKILL.md`
- `docs/zh-CN/skills/*/SKILL.md`
- `docs/tr/skills/*/SKILL.md`
- `.claude/commands/add-or-update-skill.md`
- `.claude/skills/*/SKILL.md`
- `.agents/skills/*/SKILL.md`
- `.agents/skills/*/agents/*.yaml`
## Suggested Sequence
@@ -27,8 +28,10 @@ Adds a new skill or updates an existing skill, including documentation and confi
## Typical Commit Signals
- Create or update SKILL.md in the appropriate skills/ directory
- Optionally update related documentation or integration files
- Create or update .claude/commands/add-or-update-skill.md
- Create or update .claude/skills/<skill-name>/SKILL.md
- Optionally update .agents/skills/<skill-name>/SKILL.md
- Optionally update .agents/skills/<skill-name>/agents/*.yaml
## Notes

View File

@@ -2,7 +2,7 @@
"version": "1.3",
"schemaVersion": "1.0",
"generatedBy": "ecc-tools",
"generatedAt": "2026-03-24T10:43:58.912Z",
"generatedAt": "2026-03-24T10:43:54.594Z",
"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-or-update-ecc-command-doc.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-or-update-ecc-command-doc.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-or-update-ecc-command-doc.md"
".claude/commands/add-or-update-skill.md"
]
},
"files": [
@@ -297,8 +297,8 @@
},
{
"moduleId": "workflow-pack",
"path": ".claude/commands/add-or-update-ecc-command-doc.md",
"description": "Workflow command scaffold for add-or-update-ecc-command-doc."
"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-or-update-ecc-command-doc",
"path": ".claude/commands/add-or-update-ecc-command-doc.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-or-update-ecc-command-doc.md"
".claude/commands/add-or-update-skill.md"
]
},
"codex": {

View File

@@ -10,5 +10,5 @@
"javascript"
],
"suggestedBy": "ecc-tools-repo-analysis",
"createdAt": "2026-03-24T10:44:13.997Z"
"createdAt": "2026-03-24T10:44:09.205Z"
}

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: 8
- Workflows detected: 10

View File

@@ -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-or-update-ecc-command-doc: Adds or updates documentation for an ECC command, typically as a Markdown file under .claude/commands.
- add-or-update-skill: Adds or updates a skill, including its documentation and configuration.
## Review Reminder

View File

@@ -228,77 +228,78 @@ fix: bump plugin.json and marketplace.json to v1.9.0
Add Turkish (tr) docs and update README (#744)
```
### Add Or Update Ecc Command Doc
### Add Or Update Skill
Adds or updates documentation for an ECC command, typically as a Markdown file under .claude/commands.
Adds or updates a skill, including its documentation and configuration.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update .claude/commands/add-or-update-skill.md
2. Create or update .claude/skills/<skill-name>/SKILL.md
3. Optionally update .agents/skills/<skill-name>/SKILL.md
4. Optionally update .agents/skills/<skill-name>/agents/*.yaml
**Files typically involved**:
- `.claude/commands/add-or-update-skill.md`
- `.claude/skills/*/SKILL.md`
- `.agents/skills/*/SKILL.md`
- `.agents/skills/*/agents/*.yaml`
**Example commit sequence**:
```
Create or update .claude/commands/add-or-update-skill.md
Create or update .claude/skills/<skill-name>/SKILL.md
Optionally update .agents/skills/<skill-name>/SKILL.md
Optionally update .agents/skills/<skill-name>/agents/*.yaml
```
### Add Or Update Command Documentation
Adds or updates documentation for a CLI command or workflow.
**Frequency**: ~3 times per month
**Steps**:
1. Create or update a Markdown file in .claude/commands/ describing the command.
2. Optionally, update related documentation elsewhere.
1. Create or update .claude/commands/<command-name>.md
**Files typically involved**:
- `.claude/commands/*.md`
**Example commit sequence**:
```
Create or update a Markdown file in .claude/commands/ describing the command.
Optionally, update related documentation elsewhere.
Create or update .claude/commands/<command-name>.md
```
### Add Or Update Skill Doc
### Add Or Update Database Migration Patterns
Adds or updates documentation for a skill, typically as SKILL.md under a skill directory.
**Frequency**: ~3 times per month
**Steps**:
1. Create or update SKILL.md in the relevant skill directory (e.g., skills/skill-name/SKILL.md).
2. Optionally, update related documentation or diagrams.
**Files typically involved**:
- `skills/*/SKILL.md`
- `.agents/skills/*/SKILL.md`
- `.claude/skills/*/SKILL.md`
**Example commit sequence**:
```
Create or update SKILL.md in the relevant skill directory (e.g., skills/skill-name/SKILL.md).
Optionally, update related documentation or diagrams.
```
### Add Or Update Localization Docs
Adds or updates localized documentation for agents, commands, skills, and guides in a new or existing language.
Adds or updates database migration patterns or documentation.
**Frequency**: ~2 times per month
**Steps**:
1. Add or update multiple Markdown files under docs/<lang>/ for agents, commands, skills, rules, and guides.
2. Update README.md to reflect supported languages.
1. Create or update .claude/commands/database-migration.md
2. Create or update skills/database-migrations/SKILL.md
**Files typically involved**:
- `docs/*/README.md`
- `docs/*/agents/*.md`
- `docs/*/commands/*.md`
- `docs/*/skills/*/SKILL.md`
- `docs/*/rules/**/*.md`
- `.claude/commands/database-migration.md`
- `skills/database-migrations/SKILL.md`
**Example commit sequence**:
```
Add or update multiple Markdown files under docs/<lang>/ for agents, commands, skills, rules, and guides.
Update README.md to reflect supported languages.
Create or update .claude/commands/database-migration.md
Create or update skills/database-migrations/SKILL.md
```
### Add Or Update Team Or Identity Config
Adds or updates team configuration or identity files for ECC.
Adds or updates team configuration or identity files.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update .claude/team/everything-claude-code-team-config.json or .claude/identity.json.
1. Create or update .claude/team/everything-claude-code-team-config.json
2. Create or update .claude/identity.json
**Files typically involved**:
- `.claude/team/everything-claude-code-team-config.json`
@@ -306,42 +307,79 @@ Adds or updates team configuration or identity files for ECC.
**Example commit sequence**:
```
Create or update .claude/team/everything-claude-code-team-config.json or .claude/identity.json.
Create or update .claude/team/everything-claude-code-team-config.json
Create or update .claude/identity.json
```
### Add Or Update Ecc Tools Config
### Add Or Update Guardrails Or Controls
Adds or updates the ECC tools configuration file.
Adds or updates project guardrails, rules, or enterprise controls.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update .claude/ecc-tools.json.
1. Create or update .claude/rules/everything-claude-code-guardrails.md
2. Create or update .claude/enterprise/controls.md
**Files typically involved**:
- `.claude/rules/everything-claude-code-guardrails.md`
- `.claude/enterprise/controls.md`
**Example commit sequence**:
```
Create or update .claude/rules/everything-claude-code-guardrails.md
Create or update .claude/enterprise/controls.md
```
### Add Or Update Agent Config
Adds or updates agent configuration TOML files.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update .codex/agents/*.toml
**Files typically involved**:
- `.codex/agents/*.toml`
**Example commit sequence**:
```
Create or update .codex/agents/*.toml
```
### Add Or Update Research Playbook
Adds or updates research playbooks or process documentation.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update .claude/research/everything-claude-code-research-playbook.md
**Files typically involved**:
- `.claude/research/everything-claude-code-research-playbook.md`
**Example commit sequence**:
```
Create or update .claude/research/everything-claude-code-research-playbook.md
```
### Add Or Update Ecc Tools Config
Adds or updates ECC tools configuration.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update .claude/ecc-tools.json
**Files typically involved**:
- `.claude/ecc-tools.json`
**Example commit sequence**:
```
Create or update .claude/ecc-tools.json.
```
### Add Or Update Agent Config
Adds or updates agent configuration TOML files for Codex or ECC agents.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update .codex/agents/*.toml or .agents/skills/*/agents/*.yaml.
**Files typically involved**:
- `.codex/agents/*.toml`
- `.agents/skills/*/agents/*.yaml`
**Example commit sequence**:
```
Create or update .codex/agents/*.toml or .agents/skills/*/agents/*.yaml.
Create or update .claude/ecc-tools.json
```

View File

@@ -9,7 +9,7 @@
"commandFiles": [
".claude/commands/database-migration.md",
".claude/commands/feature-development.md",
".claude/commands/add-or-update-ecc-command-doc.md"
".claude/commands/add-or-update-skill.md"
],
"updatedAt": "2026-03-24T10:43:58.912Z"
"updatedAt": "2026-03-24T10:43:54.594Z"
}

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,17 +2,7 @@ use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PaneLayout {
#[default]
Horizontal,
Vertical,
Grid,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
pub db_path: PathBuf,
pub worktree_root: PathBuf,
@@ -22,7 +12,6 @@ pub struct Config {
pub heartbeat_interval_secs: u64,
pub default_agent: String,
pub theme: Theme,
pub pane_layout: PaneLayout,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -43,7 +32,6 @@ impl Default for Config {
heartbeat_interval_secs: 30,
default_agent: "claude".to_string(),
theme: Theme::Dark,
pane_layout: PaneLayout::Horizontal,
}
}
}
@@ -64,28 +52,3 @@ impl Config {
}
}
}
#[cfg(test)]
mod tests {
use super::{Config, PaneLayout};
#[test]
fn default_config_uses_horizontal_pane_layout() {
assert_eq!(Config::default().pane_layout, PaneLayout::Horizontal);
}
#[test]
fn missing_pane_layout_deserializes_to_default() {
let cfg: Config = toml::from_str(r#"default_agent = "codex""#).unwrap();
assert_eq!(cfg.pane_layout, PaneLayout::Horizontal);
assert_eq!(cfg.default_agent, "codex");
}
#[test]
fn pane_layout_deserializes_from_toml() {
let cfg: Config = toml::from_str(r#"pane_layout = "grid""#).unwrap();
assert_eq!(cfg.pane_layout, PaneLayout::Grid);
}
}

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,4 +1,4 @@
use anyhow::Result;
use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};
use crate::session::store::StateStore;
@@ -14,6 +14,26 @@ pub struct ToolCallEvent {
}
impl ToolCallEvent {
pub fn new(
session_id: impl Into<String>,
tool_name: impl Into<String>,
input_summary: impl Into<String>,
output_summary: impl Into<String>,
duration_ms: u64,
) -> Self {
let tool_name = tool_name.into();
let input_summary = input_summary.into();
Self {
session_id: session_id.into(),
risk_score: Self::compute_risk(&tool_name, &input_summary),
tool_name,
input_summary,
output_summary: output_summary.into(),
duration_ms,
}
}
/// 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;
@@ -43,12 +63,119 @@ impl ToolCallEvent {
}
}
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(())
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolLogEntry {
pub id: i64,
pub session_id: String,
pub tool_name: String,
pub input_summary: String,
pub output_summary: String,
pub duration_ms: u64,
pub risk_score: f64,
pub timestamp: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolLogPage {
pub entries: Vec<ToolLogEntry>,
pub page: u64,
pub page_size: u64,
pub total: u64,
}
pub struct ToolLogger<'a> {
db: &'a StateStore,
}
impl<'a> ToolLogger<'a> {
pub fn new(db: &'a StateStore) -> Self {
Self { db }
}
pub fn log(&self, event: &ToolCallEvent) -> Result<ToolLogEntry> {
let timestamp = chrono::Utc::now().to_rfc3339();
self.db.insert_tool_log(
&event.session_id,
&event.tool_name,
&event.input_summary,
&event.output_summary,
event.duration_ms,
event.risk_score,
&timestamp,
)
}
pub fn query(&self, session_id: &str, page: u64, page_size: u64) -> Result<ToolLogPage> {
if page_size == 0 {
bail!("page_size must be greater than 0");
}
self.db.query_tool_logs(session_id, page.max(1), page_size)
}
}
pub fn log_tool_call(db: &StateStore, event: &ToolCallEvent) -> Result<ToolLogEntry> {
ToolLogger::new(db).log(event)
}
#[cfg(test)]
mod tests {
use super::{ToolCallEvent, ToolLogger};
use crate::session::store::StateStore;
use crate::session::{Session, SessionMetrics, SessionState};
use std::path::PathBuf;
fn test_db_path() -> PathBuf {
std::env::temp_dir().join(format!("ecc2-observability-{}.db", uuid::Uuid::new_v4()))
}
fn test_session(id: &str) -> Session {
let now = chrono::Utc::now();
Session {
id: id.to_string(),
task: "test task".to_string(),
agent_type: "claude".to_string(),
state: SessionState::Pending,
worktree: None,
created_at: now,
updated_at: now,
metrics: SessionMetrics::default(),
}
}
#[test]
fn compute_risk_caps_high_risk_bash_commands() {
let score = ToolCallEvent::compute_risk("Bash", "sudo rm -rf /tmp --force");
assert_eq!(score, 1.0);
}
#[test]
fn logger_persists_entries_and_paginates() -> anyhow::Result<()> {
let db_path = test_db_path();
let db = StateStore::open(&db_path)?;
db.insert_session(&test_session("sess-1"))?;
let logger = ToolLogger::new(&db);
logger.log(&ToolCallEvent::new("sess-1", "Read", "first", "ok", 5))?;
logger.log(&ToolCallEvent::new("sess-1", "Write", "second", "ok", 15))?;
logger.log(&ToolCallEvent::new("sess-1", "Bash", "third", "ok", 25))?;
let first_page = logger.query("sess-1", 1, 2)?;
assert_eq!(first_page.total, 3);
assert_eq!(first_page.entries.len(), 2);
assert_eq!(first_page.entries[0].tool_name, "Bash");
assert_eq!(first_page.entries[1].tool_name, "Write");
let second_page = logger.query("sess-1", 2, 2)?;
assert_eq!(second_page.total, 3);
assert_eq!(second_page.entries.len(), 1);
assert_eq!(second_page.entries[0].tool_name, "Read");
std::fs::remove_file(&db_path).ok();
Ok(())
}
}

View File

@@ -1,9 +1,10 @@
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::observability::{log_tool_call, ToolCallEvent, ToolLogEntry, ToolLogPage, ToolLogger};
use crate::worktree;
pub async fn create_session(
@@ -53,6 +54,44 @@ pub async fn stop_session(db: &StateStore, id: &str) -> Result<()> {
Ok(())
}
pub fn record_tool_call(
db: &StateStore,
session_id: &str,
tool_name: &str,
input_summary: &str,
output_summary: &str,
duration_ms: u64,
) -> Result<ToolLogEntry> {
let session = db
.get_session(session_id)?
.ok_or_else(|| anyhow::anyhow!("Session not found: {session_id}"))?;
let event = ToolCallEvent::new(
session.id.clone(),
tool_name,
input_summary,
output_summary,
duration_ms,
);
let entry = log_tool_call(db, &event)?;
db.increment_tool_calls(&session.id)?;
Ok(entry)
}
pub fn query_tool_calls(
db: &StateStore,
session_id: &str,
page: u64,
page_size: u64,
) -> Result<ToolLogPage> {
let session = db
.get_session(session_id)?
.ok_or_else(|| anyhow::anyhow!("Session not found: {session_id}"))?;
ToolLogger::new(db).query(&session.id, page, page_size)
}
pub struct SessionStatus(Session);
impl fmt::Display for SessionStatus {
@@ -74,3 +113,41 @@ impl fmt::Display for SessionStatus {
write!(f, "Updated: {}", s.updated_at)
}
}
#[cfg(test)]
mod tests {
use super::{create_session, query_tool_calls, record_tool_call};
use crate::config::Config;
use crate::session::store::StateStore;
#[tokio::test]
async fn record_tool_call_updates_session_metrics() -> anyhow::Result<()> {
let db_path =
std::env::temp_dir().join(format!("ecc2-session-manager-{}.db", uuid::Uuid::new_v4()));
let db = StateStore::open(&db_path)?;
let cfg = Config {
db_path: db_path.clone(),
..Config::default()
};
let session_id =
create_session(&db, &cfg, "implement tool logging", "claude", false).await?;
let entry = record_tool_call(&db, &session_id, "Bash", "git status", "clean worktree", 18)?;
assert_eq!(entry.session_id, session_id);
assert_eq!(entry.tool_name, "Bash");
let session = db.get_session(&session_id)?.expect("session should exist");
assert_eq!(session.metrics.tool_calls, 1);
let page = query_tool_calls(&db, &session_id[..4], 1, 10)?;
assert_eq!(page.total, 1);
assert_eq!(page.entries[0].output_summary, "clean worktree");
std::fs::remove_file(&db_path).ok();
Ok(())
}
}

View File

@@ -3,17 +3,7 @@ use rusqlite::Connection;
use std::path::Path;
use super::{Session, SessionMetrics, SessionState};
use crate::observability::ToolCallEvent;
#[derive(Debug, Clone, PartialEq)]
pub struct ToolLogEntry {
pub tool_name: String,
pub input_summary: String,
pub output_summary: String,
pub duration_ms: u64,
pub risk_score: f64,
pub timestamp: String,
}
use crate::observability::{ToolLogEntry, ToolLogPage};
pub struct StateStore {
conn: Connection,
@@ -123,6 +113,14 @@ impl StateStore {
Ok(())
}
pub fn increment_tool_calls(&self, session_id: &str) -> Result<()> {
self.conn.execute(
"UPDATE sessions SET tool_calls = tool_calls + 1, updated_at = ?1 WHERE id = ?2",
rusqlite::params![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, worktree_path, worktree_branch, worktree_base,
@@ -186,15 +184,6 @@ impl StateStore {
.find(|s| s.id == id || s.id.starts_with(id)))
}
pub fn list_tool_logs(&self, session_id: &str, limit: usize) -> Result<Vec<ToolLogEntry>> {
let table_entries = self.list_tool_logs_from_table(session_id, limit)?;
if !table_entries.is_empty() {
return Ok(table_entries);
}
self.list_tool_logs_from_messages(session_id, limit)
}
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)
@@ -204,75 +193,85 @@ impl StateStore {
Ok(())
}
fn list_tool_logs_from_table(
pub fn insert_tool_log(
&self,
session_id: &str,
limit: usize,
) -> Result<Vec<ToolLogEntry>> {
tool_name: &str,
input_summary: &str,
output_summary: &str,
duration_ms: u64,
risk_score: f64,
timestamp: &str,
) -> Result<ToolLogEntry> {
self.conn.execute(
"INSERT INTO tool_log (session_id, tool_name, input_summary, output_summary, duration_ms, risk_score, timestamp)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
rusqlite::params![
session_id,
tool_name,
input_summary,
output_summary,
duration_ms,
risk_score,
timestamp,
],
)?;
Ok(ToolLogEntry {
id: self.conn.last_insert_rowid(),
session_id: session_id.to_string(),
tool_name: tool_name.to_string(),
input_summary: input_summary.to_string(),
output_summary: output_summary.to_string(),
duration_ms,
risk_score,
timestamp: timestamp.to_string(),
})
}
pub fn query_tool_logs(
&self,
session_id: &str,
page: u64,
page_size: u64,
) -> Result<ToolLogPage> {
let page = page.max(1);
let offset = (page - 1) * page_size;
let total: u64 = self.conn.query_row(
"SELECT COUNT(*) FROM tool_log WHERE session_id = ?1",
rusqlite::params![session_id],
|row| row.get(0),
)?;
let mut stmt = self.conn.prepare(
"SELECT tool_name,
COALESCE(input_summary, ''),
COALESCE(output_summary, ''),
COALESCE(duration_ms, 0),
risk_score,
timestamp
"SELECT id, session_id, tool_name, input_summary, output_summary, duration_ms, risk_score, timestamp
FROM tool_log
WHERE session_id = ?1
ORDER BY timestamp DESC
LIMIT ?2",
ORDER BY timestamp DESC, id DESC
LIMIT ?2 OFFSET ?3",
)?;
let entries = stmt
.query_map(rusqlite::params![session_id, limit as i64], |row| {
.query_map(rusqlite::params![session_id, page_size, offset], |row| {
Ok(ToolLogEntry {
tool_name: row.get(0)?,
input_summary: row.get(1)?,
output_summary: row.get(2)?,
duration_ms: row.get(3)?,
risk_score: row.get(4)?,
timestamp: row.get(5)?,
id: row.get(0)?,
session_id: row.get(1)?,
tool_name: row.get(2)?,
input_summary: row.get::<_, Option<String>>(3)?.unwrap_or_default(),
output_summary: row.get::<_, Option<String>>(4)?.unwrap_or_default(),
duration_ms: row.get::<_, Option<u64>>(5)?.unwrap_or_default(),
risk_score: row.get::<_, Option<f64>>(6)?.unwrap_or_default(),
timestamp: row.get(7)?,
})
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(entries)
}
fn list_tool_logs_from_messages(
&self,
session_id: &str,
limit: usize,
) -> Result<Vec<ToolLogEntry>> {
let mut stmt = self.conn.prepare(
"SELECT content, timestamp
FROM messages
WHERE from_session = ?1 AND msg_type = 'tool_call'
ORDER BY timestamp DESC
LIMIT ?2",
)?;
let rows = stmt
.query_map(rusqlite::params![session_id, limit as i64], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})?
.collect::<Result<Vec<_>, _>>()?;
let entries = rows
.into_iter()
.filter_map(|(content, timestamp)| {
serde_json::from_str::<ToolCallEvent>(&content)
.ok()
.map(|event| ToolLogEntry {
tool_name: event.tool_name,
input_summary: event.input_summary,
output_summary: event.output_summary,
duration_ms: event.duration_ms,
risk_score: event.risk_score,
timestamp,
})
})
.collect();
Ok(entries)
Ok(ToolLogPage {
entries,
page,
page_size,
total,
})
}
}

View File

@@ -32,10 +32,6 @@ pub async fn run(db: StateStore, cfg: Config) -> Result<()> {
(_, KeyCode::Char('q')) => break,
(_, KeyCode::Tab) => dashboard.next_pane(),
(KeyModifiers::SHIFT, KeyCode::BackTab) => dashboard.prev_pane(),
(_, KeyCode::Char('+')) | (_, KeyCode::Char('=')) => {
dashboard.increase_pane_size()
}
(_, KeyCode::Char('-')) => dashboard.decrease_pane_size(),
(_, KeyCode::Char('j')) | (_, KeyCode::Down) => dashboard.scroll_down(),
(_, KeyCode::Char('k')) | (_, KeyCode::Up) => dashboard.scroll_up(),
(_, KeyCode::Char('n')) => dashboard.new_session(),

View File

@@ -1,78 +1,50 @@
use ratatui::{
prelude::*,
widgets::{Block, Borders, List, ListItem, Paragraph, Tabs, Wrap},
widgets::{Block, Borders, List, ListItem, Paragraph, Tabs},
};
use crate::config::{Config, PaneLayout};
use crate::session::store::{StateStore, ToolLogEntry};
use crate::config::Config;
use crate::session::store::StateStore;
use crate::session::{Session, SessionState};
const DEFAULT_PANE_SIZE_PERCENT: u16 = 35;
const DEFAULT_GRID_SIZE_PERCENT: u16 = 50;
const OUTPUT_PANE_PERCENT: u16 = 70;
const MIN_PANE_SIZE_PERCENT: u16 = 20;
const MAX_PANE_SIZE_PERCENT: u16 = 80;
const PANE_RESIZE_STEP_PERCENT: u16 = 5;
const MAX_LOG_ENTRIES: usize = 12;
pub struct Dashboard {
db: StateStore,
cfg: Config,
sessions: Vec<Session>,
logs: Vec<ToolLogEntry>,
selected_pane: Pane,
selected_session: usize,
show_help: bool,
scroll_offset: usize,
pane_size_percent: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq)]
enum Pane {
Sessions,
Output,
Metrics,
Log,
}
#[derive(Debug, Clone, Copy)]
struct PaneAreas {
sessions: Rect,
output: Rect,
metrics: Rect,
log: Option<Rect>,
}
impl Dashboard {
pub fn new(db: StateStore, cfg: Config) -> Self {
let pane_size_percent = match cfg.pane_layout {
PaneLayout::Grid => DEFAULT_GRID_SIZE_PERCENT,
PaneLayout::Horizontal | PaneLayout::Vertical => DEFAULT_PANE_SIZE_PERCENT,
};
let sessions = db.list_sessions().unwrap_or_default();
let mut dashboard = Self {
Self {
db,
cfg,
sessions,
logs: Vec::new(),
selected_pane: Pane::Sessions,
selected_session: 0,
show_help: false,
scroll_offset: 0,
pane_size_percent,
};
dashboard.refresh_logs();
dashboard
}
}
pub fn render(&self, frame: &mut Frame) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(10),
Constraint::Length(3),
Constraint::Length(3), // Header
Constraint::Min(10), // Main content
Constraint::Length(3), // Status bar
])
.split(frame.area());
@@ -81,14 +53,26 @@ impl Dashboard {
if self.show_help {
self.render_help(frame, chunks[1]);
} else {
let pane_areas = self.pane_areas(chunks[1]);
self.render_sessions(frame, pane_areas.sessions);
self.render_output(frame, pane_areas.output);
self.render_metrics(frame, pane_areas.metrics);
let main_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(35), // Session list
Constraint::Percentage(65), // Output/details
])
.split(chunks[1]);
if let Some(log_area) = pane_areas.log {
self.render_log(frame, log_area);
}
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]);
@@ -98,187 +82,126 @@ impl Dashboard {
let running = self
.sessions
.iter()
.filter(|session| session.state == SessionState::Running)
.filter(|s| s.state == SessionState::Running)
.count();
let total = self.sessions.len();
let title = format!(
" ECC 2.0 | {running} running / {total} total | {} {}% ",
self.layout_label(),
self.pane_size_percent
);
let tabs = Tabs::new(
self.visible_panes()
.iter()
.map(|pane| pane.title())
.collect::<Vec<_>>(),
)
.block(Block::default().borders(Borders::ALL).title(title))
.select(self.selected_pane_index())
.highlight_style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
);
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> = if self.sessions.is_empty() {
vec![ListItem::new("No sessions. Press 'n' to start one.")]
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 {
self.sessions
.iter()
.enumerate()
.map(|(index, session)| {
let state_icon = match session.state {
SessionState::Running => "",
SessionState::Idle => "",
SessionState::Completed => "",
SessionState::Failed => "",
SessionState::Stopped => "",
SessionState::Pending => "",
};
let style = if index == self.selected_session {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
} else {
Style::default()
};
let text = format!(
"{state_icon} {} [{}] {}",
&session.id[..8.min(session.id.len())],
session.agent_type,
session.task
);
ListItem::new(text).style(style)
})
.collect()
Style::default()
};
let list = List::new(items).block(
Block::default()
.borders(Borders::ALL)
.title(" Sessions ")
.border_style(self.pane_border_style(Pane::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.current_session() {
let worktree = session
.worktree
.as_ref()
.map(|worktree| {
format!(
"Worktree: {}\nBranch: {}\n",
worktree.path.display(),
worktree.branch
)
})
.unwrap_or_default();
let content = if let Some(session) = self.sessions.get(self.selected_session) {
format!(
"Session: {}\nAgent: {}\nState: {}\nTask: {}\nUpdated: {}\n{}\
\nLive streaming output is not wired yet. Session context is shown here until the stream viewer lands.",
session.id,
session.agent_type,
session.state,
session.task,
session.updated_at.format("%Y-%m-%d %H:%M:%S UTC"),
worktree
"Agent output for session {}...\n\n(Live streaming coming soon)",
session.id
)
} else {
"No sessions. Press 'n' to start one.".to_string()
};
let paragraph = Paragraph::new(content)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Output ")
.border_style(self.pane_border_style(Pane::Output)),
)
.scroll((self.scroll_offset_u16(), 0))
.wrap(Wrap { trim: false });
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.current_session() {
let metrics = &session.metrics;
let content = if let Some(session) = self.sessions.get(self.selected_session) {
let m = &session.metrics;
format!(
"Tokens: {}\nTools: {}\nFiles: {}\nCost: ${:.4}\nDuration: {}s",
metrics.tokens_used,
metrics.tool_calls,
metrics.files_changed,
metrics.cost_usd,
metrics.duration_secs
"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 paragraph = Paragraph::new(content)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Metrics ")
.border_style(self.pane_border_style(Pane::Metrics)),
)
.scroll((self.scroll_offset_u16(), 0))
.wrap(Wrap { trim: false });
frame.render_widget(paragraph, area);
}
fn render_log(&self, frame: &mut Frame, area: Rect) {
let content = if self.current_session().is_none() {
"No session selected".to_string()
} else if self.logs.is_empty() {
"No tool logs available for this session.\n\nTool call observability events will appear here when they are recorded."
.to_string()
let border_style = if self.selected_pane == Pane::Metrics {
Style::default().fg(Color::Cyan)
} else {
self.logs
.iter()
.map(|entry| {
format!(
"[{}] {} | {}ms | risk {:.0}%\ninput: {}\noutput: {}",
self.short_timestamp(&entry.timestamp),
entry.tool_name,
entry.duration_ms,
entry.risk_score * 100.0,
self.log_field(&entry.input_summary),
self.log_field(&entry.output_summary)
)
})
.collect::<Vec<_>>()
.join("\n\n")
Style::default()
};
let paragraph = Paragraph::new(content)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Log ")
.border_style(self.pane_border_style(Pane::Log)),
)
.scroll((self.scroll_offset_u16(), 0))
.wrap(Wrap { trim: false });
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 = format!(
" [n]ew session [s]top [Tab] switch pane [j/k] scroll [+/-] resize [{}] layout [?] help [q]uit ",
self.layout_label()
);
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));
@@ -295,8 +218,6 @@ impl Dashboard {
" S-Tab Previous pane",
" j/↓ Scroll down",
" k/↑ Scroll up",
" +/= Increase pane size",
" - Decrease pane size",
" r Refresh",
" ? Toggle help",
" q/C-c Quit",
@@ -312,46 +233,24 @@ impl Dashboard {
}
pub fn next_pane(&mut self) {
let visible_panes = self.visible_panes();
let next_index = self
.selected_pane_index()
.checked_add(1)
.map(|index| index % visible_panes.len())
.unwrap_or(0);
self.selected_pane = visible_panes[next_index];
self.scroll_offset = 0;
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) {
let visible_panes = self.visible_panes();
let previous_index = if self.selected_pane_index() == 0 {
visible_panes.len() - 1
} else {
self.selected_pane_index() - 1
self.selected_pane = match self.selected_pane {
Pane::Sessions => Pane::Metrics,
Pane::Output => Pane::Sessions,
Pane::Metrics => Pane::Output,
};
self.selected_pane = visible_panes[previous_index];
self.scroll_offset = 0;
}
pub fn increase_pane_size(&mut self) {
self.pane_size_percent =
(self.pane_size_percent + PANE_RESIZE_STEP_PERCENT).min(MAX_PANE_SIZE_PERCENT);
}
pub fn decrease_pane_size(&mut self) {
self.pane_size_percent = self
.pane_size_percent
.saturating_sub(PANE_RESIZE_STEP_PERCENT)
.max(MIN_PANE_SIZE_PERCENT);
}
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.scroll_offset = 0;
self.refresh_logs();
} else {
self.scroll_offset = self.scroll_offset.saturating_add(1);
}
@@ -359,19 +258,14 @@ impl Dashboard {
pub fn scroll_up(&mut self) {
if self.selected_pane == Pane::Sessions {
let previous_index = self.selected_session;
self.selected_session = self.selected_session.saturating_sub(1);
if self.selected_session != previous_index {
self.scroll_offset = 0;
self.refresh_logs();
}
} 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");
}
@@ -383,7 +277,7 @@ impl Dashboard {
}
pub fn refresh(&mut self) {
self.sync_from_store();
self.sessions = self.db.list_sessions().unwrap_or_default();
}
pub fn toggle_help(&mut self) {
@@ -391,254 +285,7 @@ impl Dashboard {
}
pub async fn tick(&mut self) {
self.sync_from_store();
}
fn sync_from_store(&mut self) {
// Periodic refresh every few ticks
self.sessions = self.db.list_sessions().unwrap_or_default();
self.clamp_selected_session();
self.ensure_selected_pane_visible();
self.refresh_logs();
}
fn current_session(&self) -> Option<&Session> {
self.sessions.get(self.selected_session)
}
fn refresh_logs(&mut self) {
let session_id = self.current_session().map(|session| session.id.clone());
self.logs = session_id
.and_then(|id| self.db.list_tool_logs(&id, MAX_LOG_ENTRIES).ok())
.unwrap_or_default();
}
fn clamp_selected_session(&mut self) {
if self.sessions.is_empty() {
self.selected_session = 0;
return;
}
self.selected_session = self.selected_session.min(self.sessions.len() - 1);
}
fn ensure_selected_pane_visible(&mut self) {
if !self.visible_panes().contains(&self.selected_pane) {
self.selected_pane = Pane::Sessions;
}
}
fn pane_areas(&self, area: Rect) -> PaneAreas {
match self.cfg.pane_layout {
PaneLayout::Horizontal => {
let columns = Layout::default()
.direction(Direction::Horizontal)
.constraints(self.primary_constraints())
.split(area);
let right_rows = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(OUTPUT_PANE_PERCENT),
Constraint::Percentage(100 - OUTPUT_PANE_PERCENT),
])
.split(columns[1]);
PaneAreas {
sessions: columns[0],
output: right_rows[0],
metrics: right_rows[1],
log: None,
}
}
PaneLayout::Vertical => {
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints(self.primary_constraints())
.split(area);
let bottom_columns = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(OUTPUT_PANE_PERCENT),
Constraint::Percentage(100 - OUTPUT_PANE_PERCENT),
])
.split(rows[1]);
PaneAreas {
sessions: rows[0],
output: bottom_columns[0],
metrics: bottom_columns[1],
log: None,
}
}
PaneLayout::Grid => {
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints(self.primary_constraints())
.split(area);
let top_columns = Layout::default()
.direction(Direction::Horizontal)
.constraints(self.primary_constraints())
.split(rows[0]);
let bottom_columns = Layout::default()
.direction(Direction::Horizontal)
.constraints(self.primary_constraints())
.split(rows[1]);
PaneAreas {
sessions: top_columns[0],
output: top_columns[1],
metrics: bottom_columns[0],
log: Some(bottom_columns[1]),
}
}
}
}
fn primary_constraints(&self) -> [Constraint; 2] {
[
Constraint::Percentage(self.pane_size_percent),
Constraint::Percentage(100 - self.pane_size_percent),
]
}
fn visible_panes(&self) -> &'static [Pane] {
match self.cfg.pane_layout {
PaneLayout::Grid => &[Pane::Sessions, Pane::Output, Pane::Metrics, Pane::Log],
PaneLayout::Horizontal | PaneLayout::Vertical => {
&[Pane::Sessions, Pane::Output, Pane::Metrics]
}
}
}
fn selected_pane_index(&self) -> usize {
self.visible_panes()
.iter()
.position(|pane| *pane == self.selected_pane)
.unwrap_or(0)
}
fn pane_border_style(&self, pane: Pane) -> Style {
if self.selected_pane == pane {
Style::default().fg(Color::Cyan)
} else {
Style::default()
}
}
fn layout_label(&self) -> &'static str {
match self.cfg.pane_layout {
PaneLayout::Horizontal => "horizontal",
PaneLayout::Vertical => "vertical",
PaneLayout::Grid => "grid",
}
}
fn scroll_offset_u16(&self) -> u16 {
self.scroll_offset.min(u16::MAX as usize) as u16
}
fn log_field<'a>(&self, value: &'a str) -> &'a str {
let trimmed = value.trim();
if trimmed.is_empty() {
"n/a"
} else {
trimmed
}
}
fn short_timestamp(&self, timestamp: &str) -> String {
chrono::DateTime::parse_from_rfc3339(timestamp)
.map(|value| value.format("%H:%M:%S").to_string())
.unwrap_or_else(|_| timestamp.to_string())
}
}
impl Pane {
fn title(self) -> &'static str {
match self {
Pane::Sessions => "Sessions",
Pane::Output => "Output",
Pane::Metrics => "Metrics",
Pane::Log => "Log",
}
}
}
#[cfg(test)]
mod tests {
use super::{
Dashboard, Pane, DEFAULT_GRID_SIZE_PERCENT, MAX_PANE_SIZE_PERCENT, MIN_PANE_SIZE_PERCENT,
};
use crate::config::{Config, PaneLayout};
use crate::session::store::StateStore;
use ratatui::layout::Rect;
fn dashboard_for(layout: PaneLayout) -> Dashboard {
let mut cfg = Config::default();
cfg.pane_layout = layout;
let db_path =
std::env::temp_dir().join(format!("ecc-dashboard-test-{}.db", uuid::Uuid::new_v4()));
let db = StateStore::open(&db_path).unwrap();
Dashboard::new(db, cfg)
}
#[test]
fn grid_layout_uses_four_panes_in_two_rows() {
let dashboard = dashboard_for(PaneLayout::Grid);
let areas = dashboard.pane_areas(Rect::new(0, 0, 100, 40));
let log_area = areas.log.expect("grid layout should render a log pane");
assert_eq!(areas.sessions.y, areas.output.y);
assert_eq!(areas.metrics.y, log_area.y);
assert!(areas.metrics.y > areas.sessions.y);
assert_eq!(areas.sessions.x, 0);
assert_eq!(areas.metrics.x, 0);
assert!(areas.output.x > areas.sessions.x);
assert!(log_area.x > areas.metrics.x);
}
#[test]
fn non_grid_layouts_hide_the_log_pane() {
let horizontal = dashboard_for(PaneLayout::Horizontal);
let vertical = dashboard_for(PaneLayout::Vertical);
assert!(horizontal
.pane_areas(Rect::new(0, 0, 100, 40))
.log
.is_none());
assert!(vertical.pane_areas(Rect::new(0, 0, 100, 40)).log.is_none());
}
#[test]
fn pane_navigation_includes_log_only_for_grid_layouts() {
let mut horizontal = dashboard_for(PaneLayout::Horizontal);
horizontal.next_pane();
horizontal.next_pane();
horizontal.next_pane();
assert_eq!(horizontal.selected_pane, Pane::Sessions);
let mut grid = dashboard_for(PaneLayout::Grid);
grid.next_pane();
grid.next_pane();
grid.next_pane();
assert_eq!(grid.selected_pane, Pane::Log);
}
#[test]
fn pane_resize_clamps_to_supported_bounds() {
let mut dashboard = dashboard_for(PaneLayout::Grid);
assert_eq!(dashboard.pane_size_percent, DEFAULT_GRID_SIZE_PERCENT);
for _ in 0..20 {
dashboard.increase_pane_size();
}
assert_eq!(dashboard.pane_size_percent, MAX_PANE_SIZE_PERCENT);
for _ in 0..40 {
dashboard.decrease_pane_size();
}
assert_eq!(dashboard.pane_size_percent, MIN_PANE_SIZE_PERCENT);
}
}

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,