Compare commits

..

46 Commits

Author SHA1 Message Date
ecc-tools[bot]
ba43de5dd0 feat: add everything-claude-code ECC bundle (.claude/commands/add-or-update-ecc-command-doc.md) 2026-03-24 10:44:29 +00:00
ecc-tools[bot]
288ffa4c1e feat: add everything-claude-code ECC bundle (.claude/commands/feature-development.md) 2026-03-24 10:44:28 +00:00
ecc-tools[bot]
9f33a2b556 feat: add everything-claude-code ECC bundle (.claude/commands/database-migration.md) 2026-03-24 10:44:27 +00:00
ecc-tools[bot]
707b360fe5 feat: add everything-claude-code ECC bundle (.claude/enterprise/controls.md) 2026-03-24 10:44:26 +00:00
ecc-tools[bot]
1f64ee1cde feat: add everything-claude-code ECC bundle (.claude/team/everything-claude-code-team-config.json) 2026-03-24 10:44:25 +00:00
ecc-tools[bot]
471554e2fd feat: add everything-claude-code ECC bundle (.claude/research/everything-claude-code-research-playbook.md) 2026-03-24 10:44:24 +00:00
ecc-tools[bot]
06ca450291 feat: add everything-claude-code ECC bundle (.claude/rules/everything-claude-code-guardrails.md) 2026-03-24 10:44:24 +00:00
ecc-tools[bot]
6dc955fab5 feat: add everything-claude-code ECC bundle (.codex/agents/docs-researcher.toml) 2026-03-24 10:44:22 +00:00
ecc-tools[bot]
e373b4d27e feat: add everything-claude-code ECC bundle (.codex/agents/reviewer.toml) 2026-03-24 10:44:21 +00:00
ecc-tools[bot]
c77a5b825b feat: add everything-claude-code ECC bundle (.codex/agents/explorer.toml) 2026-03-24 10:44:21 +00:00
ecc-tools[bot]
2a2ac42ad9 feat: add everything-claude-code ECC bundle (.claude/identity.json) 2026-03-24 10:44:19 +00:00
ecc-tools[bot]
fe0546d134 feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/agents/openai.yaml) 2026-03-24 10:44:19 +00:00
ecc-tools[bot]
7adef06fd1 feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/SKILL.md) 2026-03-24 10:44:18 +00:00
ecc-tools[bot]
bb87544ce9 feat: add everything-claude-code ECC bundle (.claude/skills/everything-claude-code/SKILL.md) 2026-03-24 10:44:17 +00:00
ecc-tools[bot]
240eb0a356 feat: add everything-claude-code ECC bundle (.claude/ecc-tools.json) 2026-03-24 10:44:16 +00:00
ecc-tools[bot]
bb06e9f557 feat: add everything-claude-code ECC bundle (.claude/commands/add-or-update-skill.md) 2026-03-24 10:43:40 +00:00
ecc-tools[bot]
fb17de2425 feat: add everything-claude-code ECC bundle (.claude/commands/feature-development.md) 2026-03-24 10:43:39 +00:00
ecc-tools[bot]
7762026d9e feat: add everything-claude-code ECC bundle (.claude/commands/database-migration.md) 2026-03-24 10:43:38 +00:00
ecc-tools[bot]
49df1d3007 feat: add everything-claude-code ECC bundle (.claude/enterprise/controls.md) 2026-03-24 10:43:37 +00:00
ecc-tools[bot]
5a5e09a7fc feat: add everything-claude-code ECC bundle (.claude/team/everything-claude-code-team-config.json) 2026-03-24 10:43:37 +00:00
ecc-tools[bot]
2e897de270 feat: add everything-claude-code ECC bundle (.claude/research/everything-claude-code-research-playbook.md) 2026-03-24 10:43:36 +00:00
ecc-tools[bot]
256dd55cfd feat: add everything-claude-code ECC bundle (.claude/rules/everything-claude-code-guardrails.md) 2026-03-24 10:43:35 +00:00
ecc-tools[bot]
3ad2db4f19 feat: add everything-claude-code ECC bundle (.codex/agents/docs-researcher.toml) 2026-03-24 10:43:34 +00:00
ecc-tools[bot]
e1ec37f40a feat: add everything-claude-code ECC bundle (.codex/agents/reviewer.toml) 2026-03-24 10:43:33 +00:00
ecc-tools[bot]
f685e0eab5 feat: add everything-claude-code ECC bundle (.codex/agents/explorer.toml) 2026-03-24 10:43:32 +00:00
ecc-tools[bot]
e4883ef634 feat: add everything-claude-code ECC bundle (.claude/identity.json) 2026-03-24 10:43:31 +00:00
ecc-tools[bot]
fe0dfc108a feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/agents/openai.yaml) 2026-03-24 10:43:30 +00:00
ecc-tools[bot]
78a6a91839 feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/SKILL.md) 2026-03-24 10:43:29 +00:00
ecc-tools[bot]
0ce8b82fbb feat: add everything-claude-code ECC bundle (.claude/skills/everything-claude-code/SKILL.md) 2026-03-24 10:43:29 +00:00
ecc-tools[bot]
2f8abe36c1 feat: add everything-claude-code ECC bundle (.claude/ecc-tools.json) 2026-03-24 10:43:28 +00:00
ecc-tools[bot]
0d72a80099 feat: add everything-claude-code ECC bundle (.claude/commands/add-or-update-skill-documentation.md) 2026-03-24 10:42:48 +00:00
ecc-tools[bot]
a13930a1b4 feat: add everything-claude-code ECC bundle (.claude/commands/feature-development.md) 2026-03-24 10:42:47 +00:00
ecc-tools[bot]
cf95bb9870 feat: add everything-claude-code ECC bundle (.claude/commands/database-migration.md) 2026-03-24 10:42:46 +00:00
ecc-tools[bot]
57a55733ba feat: add everything-claude-code ECC bundle (.claude/enterprise/controls.md) 2026-03-24 10:42:45 +00:00
ecc-tools[bot]
3999f99ea3 feat: add everything-claude-code ECC bundle (.claude/team/everything-claude-code-team-config.json) 2026-03-24 10:42:44 +00:00
ecc-tools[bot]
b7e295a3bd feat: add everything-claude-code ECC bundle (.claude/research/everything-claude-code-research-playbook.md) 2026-03-24 10:42:43 +00:00
ecc-tools[bot]
93dc7e8fd0 feat: add everything-claude-code ECC bundle (.claude/rules/everything-claude-code-guardrails.md) 2026-03-24 10:42:42 +00:00
ecc-tools[bot]
cd50de7423 feat: add everything-claude-code ECC bundle (.codex/agents/docs-researcher.toml) 2026-03-24 10:42:42 +00:00
ecc-tools[bot]
0366bdfdeb feat: add everything-claude-code ECC bundle (.codex/agents/reviewer.toml) 2026-03-24 10:42:41 +00:00
ecc-tools[bot]
99db2f4928 feat: add everything-claude-code ECC bundle (.codex/agents/explorer.toml) 2026-03-24 10:42:40 +00:00
ecc-tools[bot]
38fbff0d83 feat: add everything-claude-code ECC bundle (.claude/identity.json) 2026-03-24 10:42:39 +00:00
ecc-tools[bot]
b6f633b810 feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/agents/openai.yaml) 2026-03-24 10:42:38 +00:00
ecc-tools[bot]
34149411fa feat: add everything-claude-code ECC bundle (.agents/skills/everything-claude-code/SKILL.md) 2026-03-24 10:42:37 +00:00
ecc-tools[bot]
24c0aac4df feat: add everything-claude-code ECC bundle (.claude/skills/everything-claude-code/SKILL.md) 2026-03-24 10:42:36 +00:00
ecc-tools[bot]
f59a9ebcea feat: add everything-claude-code ECC bundle (.claude/ecc-tools.json) 2026-03-24 10:42:35 +00:00
Affaan Mustafa
ffc2c662a7 feat(ecc2): add split-pane dashboard resizing 2026-03-24 03:39:53 -07:00
19 changed files with 871 additions and 667 deletions

View File

@@ -228,126 +228,120 @@ fix: bump plugin.json and marketplace.json to v1.9.0
Add Turkish (tr) docs and update README (#744)
```
### Add Or Update Skill
### Add Or Update Ecc Command Doc
Adds or updates a skill in the ECC system, including documentation and provenance.
Adds or updates documentation for an ECC command, typically as a Markdown file under .claude/commands.
**Frequency**: ~3 times per month
**Steps**:
1. Create or update SKILL.md in the appropriate skills directory (e.g., skills/{skill-name}/SKILL.md or .claude/skills/{skill-name}/SKILL.md)
2. Optionally add or update agent YAML files (e.g., .agents/skills/{skill-name}/agents/*.yaml)
3. Optionally update provenance or placement policy files (e.g., schemas/provenance.schema.json, docs/SKILL-PLACEMENT-POLICY.md)
1. Create or update a Markdown file in .claude/commands/ describing the command.
2. Optionally, update related documentation elsewhere.
**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.
```
### Add Or Update Skill Doc
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`
- `.claude/skills/*/SKILL.md`
- `.agents/skills/*/SKILL.md`
- `.agents/skills/*/agents/*.yaml`
- `schemas/provenance.schema.json`
- `docs/SKILL-PLACEMENT-POLICY.md`
**Example commit sequence**:
```
Create or update SKILL.md in the appropriate skills directory (e.g., skills/{skill-name}/SKILL.md or .claude/skills/{skill-name}/SKILL.md)
Optionally add or update agent YAML files (e.g., .agents/skills/{skill-name}/agents/*.yaml)
Optionally update provenance or placement policy files (e.g., schemas/provenance.schema.json, docs/SKILL-PLACEMENT-POLICY.md)
```
### Add Or Update Command Documentation
Adds or updates documentation for a command, often as part of a new feature or workflow.
**Frequency**: ~4 times per month
**Steps**:
1. Create or update .claude/commands/{command-name}.md
2. Optionally update localized docs (e.g., docs/zh-CN/commands/{command-name}.md, docs/tr/commands/{command-name}.md, docs/pt-BR/commands/{command-name}.md)
**Files typically involved**:
- `.claude/commands/*.md`
- `docs/zh-CN/commands/*.md`
- `docs/tr/commands/*.md`
- `docs/pt-BR/commands/*.md`
**Example commit sequence**:
```
Create or update .claude/commands/{command-name}.md
Optionally update localized docs (e.g., docs/zh-CN/commands/{command-name}.md, docs/tr/commands/{command-name}.md, docs/pt-BR/commands/{command-name}.md)
```
### Add Or Update Localized Documentation
Adds or updates documentation in a new or existing language (localization).
**Frequency**: ~2 times per month
**Steps**:
1. Add or update files in docs/{locale}/ (where locale is zh-CN, tr, pt-BR, etc.)
2. Update README.md to reflect new language support
**Files typically involved**:
- `docs/zh-CN/**/*`
- `docs/tr/**/*`
- `docs/pt-BR/**/*`
- `README.md`
**Example commit sequence**:
```
Add or update files in docs/{locale}/ (where locale is zh-CN, tr, pt-BR, etc.)
Update README.md to reflect new language support
```
### Update Or Add Hooks
Adds or updates hooks for validation, config protection, or workflow automation.
**Frequency**: ~2 times per month
**Steps**:
1. Edit hooks/hooks.json to add or update hook definitions
2. Create or update scripts/hooks/*.js or .ts for hook logic
3. Optionally update plugin files (e.g., .opencode/plugins/ecc-hooks.ts)
**Files typically involved**:
- `hooks/hooks.json`
- `scripts/hooks/*.js`
- `.opencode/plugins/*.ts`
**Example commit sequence**:
```
Edit hooks/hooks.json to add or update hook definitions
Create or update scripts/hooks/*.js or .ts for hook logic
Optionally update plugin files (e.g., .opencode/plugins/ecc-hooks.ts)
```
### Add Or Update Ecc Bundle
Adds or updates a set of ECC configuration, command, or skill files as a bundle.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update files in .claude/ (commands, skills, rules, team, identity, etc.)
2. Optionally update .codex/agents/*.toml and .agents/skills/*
3. Repeat for each bundle component as needed
**Files typically involved**:
- `.claude/commands/*.md`
- `.claude/skills/*/SKILL.md`
- `.claude/rules/*.md`
- `.claude/team/*.json`
**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.
**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.
**Files typically involved**:
- `docs/*/README.md`
- `docs/*/agents/*.md`
- `docs/*/commands/*.md`
- `docs/*/skills/*/SKILL.md`
- `docs/*/rules/**/*.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.
```
### Add Or Update Team Or Identity Config
Adds or updates team configuration or identity files for ECC.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update .claude/team/everything-claude-code-team-config.json or .claude/identity.json.
**Files typically involved**:
- `.claude/team/everything-claude-code-team-config.json`
- `.claude/identity.json`
**Example commit sequence**:
```
Create or update .claude/team/everything-claude-code-team-config.json or .claude/identity.json.
```
### Add Or Update Ecc Tools Config
Adds or updates the ECC tools configuration file.
**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/*/SKILL.md`
- `.agents/skills/*/agents/*.yaml`
**Example commit sequence**:
```
Create or update files in .claude/ (commands, skills, rules, team, identity, etc.)
Optionally update .codex/agents/*.toml and .agents/skills/*
Repeat for each bundle component as needed
Create or update .codex/agents/*.toml or .agents/skills/*/agents/*.yaml.
```

View File

@@ -0,0 +1,34 @@
---
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,11 +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 in the form of a SKILL.md file under skills/ or skills/*/SKILL.md.
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.
## Common Files
- `skills/*/SKILL.md`
- `docs/*/skills/*/SKILL.md`
- `AGENTS.md`
- `README.md`
@@ -27,9 +28,9 @@ Adds a new skill or updates documentation for an existing skill, typically in th
## Typical Commit Signals
- Create or update skills/<skill-name>/SKILL.md
- Optionally update AGENTS.md or README.md to reflect new skill
- Optionally add architecture diagrams or implementation notes
- 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
## Notes

View File

@@ -10,16 +10,13 @@ Use this workflow when working on **add-or-update-skill** in `everything-claude-
## Goal
Adds or updates a skill in the ECC system, including documentation and provenance.
Adds a new skill or updates an existing skill, including documentation and configuration.
## Common Files
- `skills/*/SKILL.md`
- `.claude/skills/*/SKILL.md`
- `.agents/skills/*/SKILL.md`
- `.agents/skills/*/agents/*.yaml`
- `schemas/provenance.schema.json`
- `docs/SKILL-PLACEMENT-POLICY.md`
- `docs/zh-CN/skills/*/SKILL.md`
- `docs/tr/skills/*/SKILL.md`
## Suggested Sequence
@@ -30,9 +27,8 @@ Adds or updates a skill in the ECC system, including documentation and provenanc
## Typical Commit Signals
- Create or update SKILL.md in the appropriate skills directory (e.g., skills/{skill-name}/SKILL.md or .claude/skills/{skill-name}/SKILL.md)
- Optionally add or update agent YAML files (e.g., .agents/skills/{skill-name}/agents/*.yaml)
- Optionally update provenance or placement policy files (e.g., schemas/provenance.schema.json, docs/SKILL-PLACEMENT-POLICY.md)
- Create or update SKILL.md in the appropriate skills/ directory
- Optionally update related documentation or integration files
## Notes

View File

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

View File

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

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

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

View File

@@ -228,126 +228,120 @@ fix: bump plugin.json and marketplace.json to v1.9.0
Add Turkish (tr) docs and update README (#744)
```
### Add Or Update Skill
### Add Or Update Ecc Command Doc
Adds or updates a skill in the ECC system, including documentation and provenance.
Adds or updates documentation for an ECC command, typically as a Markdown file under .claude/commands.
**Frequency**: ~3 times per month
**Steps**:
1. Create or update SKILL.md in the appropriate skills directory (e.g., skills/{skill-name}/SKILL.md or .claude/skills/{skill-name}/SKILL.md)
2. Optionally add or update agent YAML files (e.g., .agents/skills/{skill-name}/agents/*.yaml)
3. Optionally update provenance or placement policy files (e.g., schemas/provenance.schema.json, docs/SKILL-PLACEMENT-POLICY.md)
1. Create or update a Markdown file in .claude/commands/ describing the command.
2. Optionally, update related documentation elsewhere.
**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.
```
### Add Or Update Skill Doc
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`
- `.claude/skills/*/SKILL.md`
- `.agents/skills/*/SKILL.md`
- `.agents/skills/*/agents/*.yaml`
- `schemas/provenance.schema.json`
- `docs/SKILL-PLACEMENT-POLICY.md`
**Example commit sequence**:
```
Create or update SKILL.md in the appropriate skills directory (e.g., skills/{skill-name}/SKILL.md or .claude/skills/{skill-name}/SKILL.md)
Optionally add or update agent YAML files (e.g., .agents/skills/{skill-name}/agents/*.yaml)
Optionally update provenance or placement policy files (e.g., schemas/provenance.schema.json, docs/SKILL-PLACEMENT-POLICY.md)
```
### Add Or Update Command Documentation
Adds or updates documentation for a command, often as part of a new feature or workflow.
**Frequency**: ~4 times per month
**Steps**:
1. Create or update .claude/commands/{command-name}.md
2. Optionally update localized docs (e.g., docs/zh-CN/commands/{command-name}.md, docs/tr/commands/{command-name}.md, docs/pt-BR/commands/{command-name}.md)
**Files typically involved**:
- `.claude/commands/*.md`
- `docs/zh-CN/commands/*.md`
- `docs/tr/commands/*.md`
- `docs/pt-BR/commands/*.md`
**Example commit sequence**:
```
Create or update .claude/commands/{command-name}.md
Optionally update localized docs (e.g., docs/zh-CN/commands/{command-name}.md, docs/tr/commands/{command-name}.md, docs/pt-BR/commands/{command-name}.md)
```
### Add Or Update Localized Documentation
Adds or updates documentation in a new or existing language (localization).
**Frequency**: ~2 times per month
**Steps**:
1. Add or update files in docs/{locale}/ (where locale is zh-CN, tr, pt-BR, etc.)
2. Update README.md to reflect new language support
**Files typically involved**:
- `docs/zh-CN/**/*`
- `docs/tr/**/*`
- `docs/pt-BR/**/*`
- `README.md`
**Example commit sequence**:
```
Add or update files in docs/{locale}/ (where locale is zh-CN, tr, pt-BR, etc.)
Update README.md to reflect new language support
```
### Update Or Add Hooks
Adds or updates hooks for validation, config protection, or workflow automation.
**Frequency**: ~2 times per month
**Steps**:
1. Edit hooks/hooks.json to add or update hook definitions
2. Create or update scripts/hooks/*.js or .ts for hook logic
3. Optionally update plugin files (e.g., .opencode/plugins/ecc-hooks.ts)
**Files typically involved**:
- `hooks/hooks.json`
- `scripts/hooks/*.js`
- `.opencode/plugins/*.ts`
**Example commit sequence**:
```
Edit hooks/hooks.json to add or update hook definitions
Create or update scripts/hooks/*.js or .ts for hook logic
Optionally update plugin files (e.g., .opencode/plugins/ecc-hooks.ts)
```
### Add Or Update Ecc Bundle
Adds or updates a set of ECC configuration, command, or skill files as a bundle.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update files in .claude/ (commands, skills, rules, team, identity, etc.)
2. Optionally update .codex/agents/*.toml and .agents/skills/*
3. Repeat for each bundle component as needed
**Files typically involved**:
- `.claude/commands/*.md`
- `.claude/skills/*/SKILL.md`
- `.claude/rules/*.md`
- `.claude/team/*.json`
**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.
**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.
**Files typically involved**:
- `docs/*/README.md`
- `docs/*/agents/*.md`
- `docs/*/commands/*.md`
- `docs/*/skills/*/SKILL.md`
- `docs/*/rules/**/*.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.
```
### Add Or Update Team Or Identity Config
Adds or updates team configuration or identity files for ECC.
**Frequency**: ~2 times per month
**Steps**:
1. Create or update .claude/team/everything-claude-code-team-config.json or .claude/identity.json.
**Files typically involved**:
- `.claude/team/everything-claude-code-team-config.json`
- `.claude/identity.json`
**Example commit sequence**:
```
Create or update .claude/team/everything-claude-code-team-config.json or .claude/identity.json.
```
### Add Or Update Ecc Tools Config
Adds or updates the ECC tools configuration file.
**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/*/SKILL.md`
- `.agents/skills/*/agents/*.yaml`
**Example commit sequence**:
```
Create or update files in .claude/ (commands, skills, rules, team, identity, etc.)
Optionally update .codex/agents/*.toml and .agents/skills/*
Repeat for each bundle component as needed
Create or update .codex/agents/*.toml or .agents/skills/*/agents/*.yaml.
```

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,17 @@ 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,
}
pub struct StateStore {
conn: Connection,
@@ -175,6 +186,15 @@ 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)
@@ -183,4 +203,76 @@ impl StateStore {
)?;
Ok(())
}
fn list_tool_logs_from_table(
&self,
session_id: &str,
limit: usize,
) -> Result<Vec<ToolLogEntry>> {
let mut stmt = self.conn.prepare(
"SELECT tool_name,
COALESCE(input_summary, ''),
COALESCE(output_summary, ''),
COALESCE(duration_ms, 0),
risk_score,
timestamp
FROM tool_log
WHERE session_id = ?1
ORDER BY timestamp DESC
LIMIT ?2",
)?;
let entries = stmt
.query_map(rusqlite::params![session_id, limit as i64], |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)?,
})
})?
.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)
}
}

View File

@@ -32,6 +32,10 @@ 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,50 +1,78 @@
use ratatui::{
prelude::*,
widgets::{Block, Borders, List, ListItem, Paragraph, Tabs},
widgets::{Block, Borders, List, ListItem, Paragraph, Tabs, Wrap},
};
use crate::config::Config;
use crate::session::store::StateStore;
use crate::config::{Config, PaneLayout};
use crate::session::store::{StateStore, ToolLogEntry};
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)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
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();
Self {
let mut dashboard = 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), // Header
Constraint::Min(10), // Main content
Constraint::Length(3), // Status bar
Constraint::Length(3),
Constraint::Min(10),
Constraint::Length(3),
])
.split(frame.area());
@@ -53,26 +81,14 @@ impl Dashboard {
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]);
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);
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]);
if let Some(log_area) = pane_areas.log {
self.render_log(frame, log_area);
}
}
self.render_status_bar(frame, chunks[2]);
@@ -82,126 +98,187 @@ impl Dashboard {
let running = self
.sessions
.iter()
.filter(|s| s.state == SessionState::Running)
.filter(|session| session.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 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),
);
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),
);
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)
let items: Vec<ListItem> = if self.sessions.is_empty() {
vec![ListItem::new("No sessions. Press 'n' to start one.")]
} else {
Style::default()
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()
};
let list = List::new(items).block(
Block::default()
.borders(Borders::ALL)
.title(" Sessions ")
.border_style(border_style),
.border_style(self.pane_border_style(Pane::Sessions)),
);
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) {
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();
format!(
"Agent output for session {}...\n\n(Live streaming coming soon)",
session.id
"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
)
} 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),
);
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 });
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;
let content = if let Some(session) = self.current_session() {
let metrics = &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
"Tokens: {}\nTools: {}\nFiles: {}\nCost: ${:.4}\nDuration: {}s",
metrics.tokens_used,
metrics.tool_calls,
metrics.files_changed,
metrics.cost_usd,
metrics.duration_secs
)
} else {
"No metrics available".to_string()
};
let border_style = if self.selected_pane == Pane::Metrics {
Style::default().fg(Color::Cyan)
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()
} else {
Style::default()
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")
};
let paragraph = Paragraph::new(content).block(
Block::default()
.borders(Borders::ALL)
.title(" Metrics ")
.border_style(border_style),
);
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 });
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 text = format!(
" [n]ew session [s]top [Tab] switch pane [j/k] scroll [+/-] resize [{}] layout [?] help [q]uit ",
self.layout_label()
);
let paragraph = Paragraph::new(text)
.style(Style::default().fg(Color::DarkGray))
.block(Block::default().borders(Borders::ALL));
@@ -218,6 +295,8 @@ 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",
@@ -233,24 +312,46 @@ impl Dashboard {
}
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,
};
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;
}
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,
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 = 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);
}
@@ -258,14 +359,19 @@ 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");
}
@@ -277,7 +383,7 @@ impl Dashboard {
}
pub fn refresh(&mut self) {
self.sessions = self.db.list_sessions().unwrap_or_default();
self.sync_from_store();
}
pub fn toggle_help(&mut self) {
@@ -285,7 +391,254 @@ impl Dashboard {
}
pub async fn tick(&mut self) {
// Periodic refresh every few ticks
self.sync_from_store();
}
fn sync_from_store(&mut self) {
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,11 +28,7 @@ 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,