Compare commits

..

46 Commits

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

View File

@@ -228,120 +228,95 @@ 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 Command Doc
Adds or updates documentation for an ECC command, typically as a Markdown file under .claude/commands.
Adds or updates documentation for a command, typically in Markdown under a language or locale-specific docs directory.
**Frequency**: ~3 times per month
**Steps**:
1. Create or update a Markdown file in .claude/commands/ describing the command.
2. Optionally, update related documentation elsewhere.
1. Create or update a Markdown file for the command in the appropriate docs directory (e.g., docs/zh-CN/commands/ or docs/tr/commands/).
2. Commit the new or changed file with a message referencing the command.
3. Optionally update README or language count if adding a new language.
**Files typically involved**:
- `.claude/commands/*.md`
- `docs/zh-CN/commands/*.md`
- `docs/tr/commands/*.md`
- `docs/pt-BR/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 a Markdown file for the command in the appropriate docs directory (e.g., docs/zh-CN/commands/ or docs/tr/commands/).
Commit the new or changed file with a message referencing the command.
Optionally update README or language count if adding a new language.
```
### Add Or Update Skill Doc
Adds or updates documentation for a skill, typically as SKILL.md under a skill directory.
Adds or updates documentation for a skill, typically in SKILL.md under a language or locale-specific docs directory.
**Frequency**: ~3 times per month
**Steps**:
1. Create or update SKILL.md in the relevant skill directory (e.g., skills/skill-name/SKILL.md).
2. Optionally, update related documentation or diagrams.
1. Create or update a SKILL.md file for the skill in the appropriate docs directory (e.g., docs/zh-CN/skills/, docs/tr/skills/, docs/pt-BR/skills/).
2. Commit the new or changed file with a message referencing the skill.
**Files typically involved**:
- `skills/*/SKILL.md`
- `.agents/skills/*/SKILL.md`
- `.claude/skills/*/SKILL.md`
- `docs/zh-CN/skills/*/SKILL.md`
- `docs/tr/skills/*/SKILL.md`
- `docs/pt-BR/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.
Create or update a SKILL.md file for the skill in the appropriate docs directory (e.g., docs/zh-CN/skills/, docs/tr/skills/, docs/pt-BR/skills/).
Commit the new or changed file with a message referencing the skill.
```
### Add Or Update Localization Docs
### Add Or Update Locale Docs
Adds or updates localized documentation for agents, commands, skills, and guides in a new or existing language.
Adds or updates a full set of localized documentation for a new or existing language, including agents, commands, skills, and guides.
**Frequency**: ~2 times per month
**Steps**:
1. 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 multiple Markdown files under a new or existing language directory (e.g., docs/tr/, docs/pt-BR/, docs/zh-CN/).
2. Update README.md to increment supported language count and add references.
3. Commit all new or changed files.
**Files typically involved**:
- `docs/*/README.md`
- `docs/*/agents/*.md`
- `docs/*/commands/*.md`
- `docs/*/skills/*/SKILL.md`
- `docs/*/rules/**/*.md`
- `docs/tr/**/*`
- `docs/pt-BR/**/*`
- `docs/zh-CN/**/*`
- `README.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 multiple Markdown files under a new or existing language directory (e.g., docs/tr/, docs/pt-BR/, docs/zh-CN/).
Update README.md to increment supported language count and add references.
Commit all new or changed files.
```
### Add Or Update Team Or Identity Config
### Add Or Update Ecc Bundle Command
Adds or updates team configuration or identity files for ECC.
Adds or updates ECC bundle command documentation or configuration, typically in .claude/commands/ or related ECC config directories.
**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 a Markdown file in .claude/commands/ for the new or updated command.
2. Optionally update related config files (e.g., .claude/ecc-tools.json, .claude/identity.json).
3. Commit the changes.
**Files typically involved**:
- `.claude/team/everything-claude-code-team-config.json`
- `.claude/commands/*.md`
- `.claude/ecc-tools.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/*/agents/*.yaml`
**Example commit sequence**:
```
Create or update .codex/agents/*.toml or .agents/skills/*/agents/*.yaml.
Create or update a Markdown file in .claude/commands/ for the new or updated command.
Optionally update related config files (e.g., .claude/ecc-tools.json, .claude/identity.json).
Commit the changes.
```

View File

@@ -0,0 +1,37 @@
---
name: add-or-update-command-doc
description: Workflow command scaffold for add-or-update-command-doc in everything-claude-code.
allowed_tools: ["Bash", "Read", "Write", "Grep", "Glob"]
---
# /add-or-update-command-doc
Use this workflow when working on **add-or-update-command-doc** in `everything-claude-code`.
## Goal
Adds or updates documentation for a command, typically in Markdown under a language or locale-specific docs directory.
## Common Files
- `docs/zh-CN/commands/*.md`
- `docs/tr/commands/*.md`
- `docs/pt-BR/commands/*.md`
## Suggested Sequence
1. Understand the current state and failure mode before editing.
2. Make the smallest coherent change that satisfies the workflow goal.
3. Run the most relevant verification for touched files.
4. Summarize what changed and what still needs review.
## Typical Commit Signals
- Create or update a Markdown file for the command in the appropriate docs directory (e.g., docs/zh-CN/commands/ or docs/tr/commands/).
- Commit the new or changed file with a message referencing the command.
- Optionally update README or language count if adding a new language.
## Notes
- Treat this as a scaffold, not a hard-coded script.
- Update the command if the workflow evolves materially.

View File

@@ -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,14 @@ 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 documentation for an existing skill, typically in the form of a SKILL.md file under skills/ or skills/*/SKILL.md, sometimes with translations in docs/xx/skills/*/SKILL.md.
## Common Files
- `skills/*/SKILL.md`
- `docs/*/skills/*/SKILL.md`
- `AGENTS.md`
- `README.md`
- `docs/zh-CN/skills/*/SKILL.md`
- `docs/tr/skills/*/SKILL.md`
- `docs/pt-BR/skills/*/SKILL.md`
## Suggested Sequence
@@ -28,9 +28,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 skills/<skill-name>/SKILL.md
- Optionally update docs/xx/skills/<skill-name>/SKILL.md for translations
- Commit with a message referencing the skill and a summary of changes
## Notes

View File

@@ -10,7 +10,7 @@ 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 a new skill or updates documentation for an existing skill.
## Common Files
@@ -27,8 +27,8 @@ 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 SKILL.md in the relevant skills directory.
- Optionally add architecture diagrams, implementation notes, or integration guidance.
## 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:44:32.276Z",
"repo": "https://github.com/affaan-m/everything-claude-code",
"profiles": {
"requested": "full",
@@ -150,7 +150,7 @@
".claude/enterprise/controls.md",
".claude/commands/database-migration.md",
".claude/commands/feature-development.md",
".claude/commands/add-or-update-ecc-command-doc.md"
".claude/commands/add-or-update-command-doc.md"
],
"packageFiles": {
"runtime-core": [
@@ -180,7 +180,7 @@
"workflow-pack": [
".claude/commands/database-migration.md",
".claude/commands/feature-development.md",
".claude/commands/add-or-update-ecc-command-doc.md"
".claude/commands/add-or-update-command-doc.md"
]
},
"moduleFiles": {
@@ -211,7 +211,7 @@
"workflow-pack": [
".claude/commands/database-migration.md",
".claude/commands/feature-development.md",
".claude/commands/add-or-update-ecc-command-doc.md"
".claude/commands/add-or-update-command-doc.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-command-doc.md",
"description": "Workflow command scaffold for add-or-update-command-doc."
}
],
"workflows": [
@@ -311,8 +311,8 @@
"path": ".claude/commands/feature-development.md"
},
{
"command": "add-or-update-ecc-command-doc",
"path": ".claude/commands/add-or-update-ecc-command-doc.md"
"command": "add-or-update-command-doc",
"path": ".claude/commands/add-or-update-command-doc.md"
}
],
"adapters": {
@@ -322,7 +322,7 @@
"commandPaths": [
".claude/commands/database-migration.md",
".claude/commands/feature-development.md",
".claude/commands/add-or-update-ecc-command-doc.md"
".claude/commands/add-or-update-command-doc.md"
]
},
"codex": {

View File

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

View File

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

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-command-doc: Adds or updates documentation for a command, typically in Markdown under a language or locale-specific docs directory.
## Review Reminder

View File

@@ -228,120 +228,95 @@ 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 Command Doc
Adds or updates documentation for an ECC command, typically as a Markdown file under .claude/commands.
Adds or updates documentation for a command, typically in Markdown under a language or locale-specific docs directory.
**Frequency**: ~3 times per month
**Steps**:
1. Create or update a Markdown file in .claude/commands/ describing the command.
2. Optionally, update related documentation elsewhere.
1. Create or update a Markdown file for the command in the appropriate docs directory (e.g., docs/zh-CN/commands/ or docs/tr/commands/).
2. Commit the new or changed file with a message referencing the command.
3. Optionally update README or language count if adding a new language.
**Files typically involved**:
- `.claude/commands/*.md`
- `docs/zh-CN/commands/*.md`
- `docs/tr/commands/*.md`
- `docs/pt-BR/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 a Markdown file for the command in the appropriate docs directory (e.g., docs/zh-CN/commands/ or docs/tr/commands/).
Commit the new or changed file with a message referencing the command.
Optionally update README or language count if adding a new language.
```
### Add Or Update Skill Doc
Adds or updates documentation for a skill, typically as SKILL.md under a skill directory.
Adds or updates documentation for a skill, typically in SKILL.md under a language or locale-specific docs directory.
**Frequency**: ~3 times per month
**Steps**:
1. Create or update SKILL.md in the relevant skill directory (e.g., skills/skill-name/SKILL.md).
2. Optionally, update related documentation or diagrams.
1. Create or update a SKILL.md file for the skill in the appropriate docs directory (e.g., docs/zh-CN/skills/, docs/tr/skills/, docs/pt-BR/skills/).
2. Commit the new or changed file with a message referencing the skill.
**Files typically involved**:
- `skills/*/SKILL.md`
- `.agents/skills/*/SKILL.md`
- `.claude/skills/*/SKILL.md`
- `docs/zh-CN/skills/*/SKILL.md`
- `docs/tr/skills/*/SKILL.md`
- `docs/pt-BR/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.
Create or update a SKILL.md file for the skill in the appropriate docs directory (e.g., docs/zh-CN/skills/, docs/tr/skills/, docs/pt-BR/skills/).
Commit the new or changed file with a message referencing the skill.
```
### Add Or Update Localization Docs
### Add Or Update Locale Docs
Adds or updates localized documentation for agents, commands, skills, and guides in a new or existing language.
Adds or updates a full set of localized documentation for a new or existing language, including agents, commands, skills, and guides.
**Frequency**: ~2 times per month
**Steps**:
1. 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 multiple Markdown files under a new or existing language directory (e.g., docs/tr/, docs/pt-BR/, docs/zh-CN/).
2. Update README.md to increment supported language count and add references.
3. Commit all new or changed files.
**Files typically involved**:
- `docs/*/README.md`
- `docs/*/agents/*.md`
- `docs/*/commands/*.md`
- `docs/*/skills/*/SKILL.md`
- `docs/*/rules/**/*.md`
- `docs/tr/**/*`
- `docs/pt-BR/**/*`
- `docs/zh-CN/**/*`
- `README.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 multiple Markdown files under a new or existing language directory (e.g., docs/tr/, docs/pt-BR/, docs/zh-CN/).
Update README.md to increment supported language count and add references.
Commit all new or changed files.
```
### Add Or Update Team Or Identity Config
### Add Or Update Ecc Bundle Command
Adds or updates team configuration or identity files for ECC.
Adds or updates ECC bundle command documentation or configuration, typically in .claude/commands/ or related ECC config directories.
**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 a Markdown file in .claude/commands/ for the new or updated command.
2. Optionally update related config files (e.g., .claude/ecc-tools.json, .claude/identity.json).
3. Commit the changes.
**Files typically involved**:
- `.claude/team/everything-claude-code-team-config.json`
- `.claude/commands/*.md`
- `.claude/ecc-tools.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/*/agents/*.yaml`
**Example commit sequence**:
```
Create or update .codex/agents/*.toml or .agents/skills/*/agents/*.yaml.
Create or update a Markdown file in .claude/commands/ for the new or updated command.
Optionally update related config files (e.g., .claude/ecc-tools.json, .claude/identity.json).
Commit the changes.
```

View File

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

1
ecc2/Cargo.lock generated
View File

@@ -332,6 +332,7 @@ dependencies = [
"crossterm",
"dirs",
"git2",
"libc",
"ratatui",
"rusqlite",
"serde",

View File

@@ -36,6 +36,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Error handling
anyhow = "1"
thiserror = "2"
libc = "0.2"
# Time
chrono = { version = "0.4", features = ["serde"] }

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;
@@ -44,6 +44,11 @@ enum Commands {
/// Session ID or alias
session_id: String,
},
/// Resume a failed or stopped session
Resume {
/// Session ID or alias
session_id: String,
},
/// Run as background daemon
Daemon,
}
@@ -63,10 +68,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) => {
@@ -84,6 +92,10 @@ async fn main() -> Result<()> {
session::manager::stop_session(&db, &session_id).await?;
println!("Session stopped: {session_id}");
}
Some(Commands::Resume { session_id }) => {
let resumed_id = session::manager::resume_session(&db, &session_id).await?;
println!("Session resumed: {resumed_id}");
}
Some(Commands::Daemon) => {
println!("Starting ECC daemon...");
session::daemon::run(db, cfg).await?;
@@ -92,3 +104,19 @@ async fn main() -> Result<()> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cli_parses_resume_command() {
let cli = Cli::try_parse_from(["ecc", "resume", "deadbeef"])
.expect("resume subcommand should parse");
match cli.command {
Some(Commands::Resume { session_id }) => assert_eq!(session_id, "deadbeef"),
_ => panic!("expected resume subcommand"),
}
}
}

View File

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

View File

@@ -1,8 +1,8 @@
use anyhow::Result;
use std::fmt;
use super::{Session, SessionMetrics, SessionState};
use super::store::StateStore;
use super::{Session, SessionMetrics, SessionState};
use crate::config::Config;
use crate::worktree;
@@ -27,6 +27,7 @@ pub async fn create_session(
task: task.to_string(),
agent_type: agent_type.to_string(),
state: SessionState::Pending,
pid: None,
worktree: wt,
created_at: now,
updated_at: now,
@@ -49,10 +50,79 @@ pub fn get_status(db: &StateStore, id: &str) -> Result<SessionStatus> {
}
pub async fn stop_session(db: &StateStore, id: &str) -> Result<()> {
db.update_state(id, &SessionState::Stopped)?;
let session = db
.get_session(id)?
.ok_or_else(|| anyhow::anyhow!("Session not found: {id}"))?;
db.update_state_and_pid(&session.id, &SessionState::Stopped, None)?;
Ok(())
}
pub async fn resume_session(db: &StateStore, id: &str) -> Result<String> {
let session = db
.get_session(id)?
.ok_or_else(|| anyhow::anyhow!("Session not found: {id}"))?;
if session.state == SessionState::Completed {
anyhow::bail!("Completed sessions cannot be resumed: {}", session.id);
}
if session.state == SessionState::Running {
anyhow::bail!("Session is already running: {}", session.id);
}
db.update_state_and_pid(&session.id, &SessionState::Pending, None)?;
Ok(session.id)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::store::StateStore;
use std::path::PathBuf;
fn temp_db_path() -> PathBuf {
std::env::temp_dir().join(format!("ecc2-manager-test-{}.db", uuid::Uuid::new_v4()))
}
fn sample_session(id: &str, state: SessionState, pid: Option<u32>) -> Session {
let now = chrono::Utc::now();
Session {
id: id.to_string(),
task: "Resume previous task".to_string(),
agent_type: "claude".to_string(),
state,
pid,
worktree: None,
created_at: now,
updated_at: now,
metrics: SessionMetrics::default(),
}
}
#[tokio::test]
async fn resume_session_requeues_failed_session() -> Result<()> {
let path = temp_db_path();
let store = StateStore::open(&path)?;
store.insert_session(&sample_session(
"deadbeef",
SessionState::Failed,
Some(31337),
))?;
let resumed_id = resume_session(&store, "deadbeef").await?;
let resumed = store
.get_session(&resumed_id)?
.expect("resumed session should exist");
assert_eq!(resumed.state, SessionState::Pending);
assert_eq!(resumed.pid, None);
let _ = std::fs::remove_file(path);
Ok(())
}
}
pub struct SessionStatus(Session);
impl fmt::Display for SessionStatus {
@@ -62,6 +132,9 @@ impl fmt::Display for SessionStatus {
writeln!(f, "Task: {}", s.task)?;
writeln!(f, "Agent: {}", s.agent_type)?;
writeln!(f, "State: {}", s.state)?;
if let Some(pid) = s.pid {
writeln!(f, "PID: {pid}")?;
}
if let Some(ref wt) = s.worktree {
writeln!(f, "Branch: {}", wt.branch)?;
writeln!(f, "Worktree: {}", wt.path.display())?;

View File

@@ -13,6 +13,7 @@ pub struct Session {
pub task: String,
pub agent_type: String,
pub state: SessionState,
pub pid: Option<u32>,
pub worktree: Option<WorktreeInfo>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,

View File

@@ -3,17 +3,6 @@ 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,
@@ -35,6 +24,7 @@ impl StateStore {
task TEXT NOT NULL,
agent_type TEXT NOT NULL,
state TEXT NOT NULL DEFAULT 'pending',
pid INTEGER,
worktree_path TEXT,
worktree_branch TEXT,
worktree_base TEXT,
@@ -73,18 +63,36 @@ impl StateStore {
CREATE INDEX IF NOT EXISTS idx_messages_to ON messages(to_session, read);
",
)?;
self.ensure_sessions_pid_column()?;
Ok(())
}
fn ensure_sessions_pid_column(&self) -> Result<()> {
let mut stmt = self.conn.prepare("PRAGMA table_info(sessions)")?;
let mut rows = stmt.query([])?;
while let Some(row) = rows.next()? {
let column_name: String = row.get(1)?;
if column_name == "pid" {
return Ok(());
}
}
self.conn
.execute("ALTER TABLE sessions ADD COLUMN pid INTEGER", [])?;
Ok(())
}
pub fn insert_session(&self, session: &Session) -> Result<()> {
self.conn.execute(
"INSERT INTO sessions (id, task, agent_type, state, worktree_path, worktree_branch, worktree_base, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
"INSERT INTO sessions (id, task, agent_type, state, pid, worktree_path, worktree_branch, worktree_base, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
rusqlite::params![
session.id,
session.task,
session.agent_type,
session.state.to_string(),
session.pid.map(i64::from),
session.worktree.as_ref().map(|w| w.path.to_string_lossy().to_string()),
session.worktree.as_ref().map(|w| w.branch.clone()),
session.worktree.as_ref().map(|w| w.base_branch.clone()),
@@ -95,6 +103,24 @@ impl StateStore {
Ok(())
}
pub fn update_state_and_pid(
&self,
session_id: &str,
state: &SessionState,
pid: Option<u32>,
) -> Result<()> {
self.conn.execute(
"UPDATE sessions SET state = ?1, pid = ?2, updated_at = ?3 WHERE id = ?4",
rusqlite::params![
state.to_string(),
pid.map(i64::from),
chrono::Utc::now().to_rfc3339(),
session_id,
],
)?;
Ok(())
}
pub fn update_state(&self, session_id: &str, state: &SessionState) -> Result<()> {
self.conn.execute(
"UPDATE sessions SET state = ?1, updated_at = ?2 WHERE id = ?3",
@@ -125,7 +151,7 @@ impl StateStore {
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,
"SELECT id, task, agent_type, state, pid, worktree_path, worktree_branch, worktree_base,
tokens_used, tool_calls, files_changed, duration_secs, cost_usd,
created_at, updated_at
FROM sessions ORDER BY updated_at DESC",
@@ -143,21 +169,26 @@ impl StateStore {
_ => SessionState::Pending,
};
let worktree_path: Option<String> = row.get(4)?;
let pid = row
.get::<_, Option<i64>>(4)?
.and_then(|value| u32::try_from(value).ok());
let worktree_path: Option<String> = row.get(5)?;
let worktree = worktree_path.map(|p| super::WorktreeInfo {
path: std::path::PathBuf::from(p),
branch: row.get::<_, String>(5).unwrap_or_default(),
base_branch: row.get::<_, String>(6).unwrap_or_default(),
branch: row.get::<_, String>(6).unwrap_or_default(),
base_branch: row.get::<_, String>(7).unwrap_or_default(),
});
let created_str: String = row.get(12)?;
let updated_str: String = row.get(13)?;
let created_str: String = row.get(13)?;
let updated_str: String = row.get(14)?;
Ok(Session {
id: row.get(0)?,
task: row.get(1)?,
agent_type: row.get(2)?,
state,
pid,
worktree,
created_at: chrono::DateTime::parse_from_rfc3339(&created_str)
.unwrap_or_default()
@@ -166,11 +197,11 @@ impl StateStore {
.unwrap_or_default()
.with_timezone(&chrono::Utc),
metrics: SessionMetrics {
tokens_used: row.get(7)?,
tool_calls: row.get(8)?,
files_changed: row.get(9)?,
duration_secs: row.get(10)?,
cost_usd: row.get(11)?,
tokens_used: row.get(8)?,
tool_calls: row.get(9)?,
files_changed: row.get(10)?,
duration_secs: row.get(11)?,
cost_usd: row.get(12)?,
},
})
})?
@@ -186,15 +217,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)
@@ -203,76 +225,97 @@ 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",
)?;
#[cfg(test)]
mod tests {
use super::*;
use crate::session::{Session, SessionMetrics, SessionState};
use std::path::PathBuf;
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 temp_db_path() -> PathBuf {
std::env::temp_dir().join(format!("ecc2-store-test-{}.db", uuid::Uuid::new_v4()))
}
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",
fn sample_session(id: &str, state: SessionState, pid: Option<u32>) -> Session {
let now = chrono::Utc::now();
Session {
id: id.to_string(),
task: "Investigate crash".to_string(),
agent_type: "claude".to_string(),
state,
pid,
worktree: None,
created_at: now,
updated_at: now,
metrics: SessionMetrics::default(),
}
}
#[test]
fn open_migrates_existing_sessions_table_with_pid_column() -> Result<()> {
let path = temp_db_path();
let conn = Connection::open(&path)?;
conn.execute_batch(
"
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
task TEXT NOT NULL,
agent_type TEXT NOT NULL,
state TEXT NOT NULL DEFAULT 'pending',
worktree_path TEXT,
worktree_branch TEXT,
worktree_base TEXT,
tokens_used INTEGER DEFAULT 0,
tool_calls INTEGER DEFAULT 0,
files_changed INTEGER DEFAULT 0,
duration_secs INTEGER DEFAULT 0,
cost_usd REAL DEFAULT 0.0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
",
)?;
conn.execute(
"INSERT INTO sessions (id, task, agent_type, state, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
rusqlite::params![
"legacy",
"Recover state",
"claude",
"running",
chrono::Utc::now().to_rfc3339(),
chrono::Utc::now().to_rfc3339(),
],
)?;
drop(conn);
let 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 store = StateStore::open(&path)?;
let session = store
.get_session("legacy")?
.expect("legacy session should load");
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();
assert_eq!(session.pid, None);
Ok(entries)
let _ = std::fs::remove_file(path);
Ok(())
}
#[test]
fn insert_session_persists_pid() -> Result<()> {
let path = temp_db_path();
let store = StateStore::open(&path)?;
let session = sample_session("abc12345", SessionState::Running, Some(4242));
store.insert_session(&session)?;
let loaded = store
.get_session("abc12345")?
.expect("session should be persisted");
assert_eq!(loaded.pid, Some(4242));
assert_eq!(loaded.state, SessionState::Running);
let _ = std::fs::remove_file(path);
Ok(())
}
}

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,31 +258,28 @@ 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");
}
pub fn stop_selected(&mut self) {
if let Some(session) = self.sessions.get(self.selected_session) {
let _ = self.db.update_state(&session.id, &SessionState::Stopped);
let _ = self
.db
.update_state_and_pid(&session.id, &SessionState::Stopped, None);
self.refresh();
}
}
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 +287,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);
}
}