mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-16 13:53:04 +08:00
Compare commits
4 Commits
8bdf88e5ad
...
fix/bash-h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a9918db00 | ||
|
|
a8bb5979a5 | ||
|
|
1c45152c6d | ||
|
|
5bfb3cc563 |
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -140,10 +140,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
case "${{ matrix.pm }}" in
|
case "${{ matrix.pm }}" in
|
||||||
npm) npm ci ;;
|
npm) npm ci ;;
|
||||||
# pnpm v10 can fail CI on ignored native build scripts
|
pnpm) pnpm install --no-frozen-lockfile ;;
|
||||||
# (for example msgpackr-extract) even though this repo is Yarn-native
|
|
||||||
# and pnpm is only exercised here as a compatibility lane.
|
|
||||||
pnpm) pnpm install --config.strict-dep-builds=false --no-frozen-lockfile ;;
|
|
||||||
# Yarn Berry (v4+) removed --ignore-engines; engine checking is no longer a core feature
|
# Yarn Berry (v4+) removed --ignore-engines; engine checking is no longer a core feature
|
||||||
yarn) yarn install ;;
|
yarn) yarn install ;;
|
||||||
bun) bun install ;;
|
bun) bun install ;;
|
||||||
|
|||||||
21
.github/workflows/release.yml
vendored
21
.github/workflows/release.yml
vendored
@@ -6,7 +6,6 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
id-token: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@@ -23,7 +22,6 @@ jobs:
|
|||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -55,23 +53,6 @@ jobs:
|
|||||||
- name: Verify release metadata stays in sync
|
- name: Verify release metadata stays in sync
|
||||||
run: node tests/plugin-manifest.test.js
|
run: node tests/plugin-manifest.test.js
|
||||||
|
|
||||||
- name: Check npm publish state
|
|
||||||
id: npm_publish_state
|
|
||||||
run: |
|
|
||||||
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
|
||||||
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
|
||||||
if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
|
|
||||||
echo "already_published=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "already_published=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Publish npm package
|
|
||||||
if: steps.npm_publish_state.outputs.already_published != 'true'
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
run: npm publish --access public --provenance
|
|
||||||
|
|
||||||
- name: Generate release highlights
|
- name: Generate release highlights
|
||||||
id: highlights
|
id: highlights
|
||||||
env:
|
env:
|
||||||
@@ -92,8 +73,6 @@ jobs:
|
|||||||
- Improved release-note generation and changelog hygiene
|
- Improved release-note generation and changelog hygiene
|
||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
- npm package: \`ecc-universal\`
|
|
||||||
- Claude marketplace/plugin identifier: \`everything-claude-code@everything-claude-code\`
|
|
||||||
- For migration tips and compatibility notes, see README and CHANGELOG.
|
- For migration tips and compatibility notes, see README and CHANGELOG.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
37
.github/workflows/reusable-release.yml
vendored
37
.github/workflows/reusable-release.yml
vendored
@@ -12,24 +12,9 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
secrets:
|
|
||||||
NPM_TOKEN:
|
|
||||||
required: false
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
tag:
|
|
||||||
description: 'Version tag to release or republish (e.g., v1.10.0)'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
generate-notes:
|
|
||||||
description: 'Auto-generate release notes'
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
id-token: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@@ -46,7 +31,6 @@ jobs:
|
|||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -78,23 +62,6 @@ jobs:
|
|||||||
- name: Verify release metadata stays in sync
|
- name: Verify release metadata stays in sync
|
||||||
run: node tests/plugin-manifest.test.js
|
run: node tests/plugin-manifest.test.js
|
||||||
|
|
||||||
- name: Check npm publish state
|
|
||||||
id: npm_publish_state
|
|
||||||
run: |
|
|
||||||
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
|
||||||
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
|
||||||
if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version >/dev/null 2>&1; then
|
|
||||||
echo "already_published=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "already_published=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Publish npm package
|
|
||||||
if: steps.npm_publish_state.outputs.already_published != 'true'
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
run: npm publish --access public --provenance
|
|
||||||
|
|
||||||
- name: Generate release highlights
|
- name: Generate release highlights
|
||||||
env:
|
env:
|
||||||
TAG_NAME: ${{ inputs.tag }}
|
TAG_NAME: ${{ inputs.tag }}
|
||||||
@@ -107,10 +74,6 @@ jobs:
|
|||||||
- Harness reliability and cross-platform compatibility
|
- Harness reliability and cross-platform compatibility
|
||||||
- Eval-driven quality improvements
|
- Eval-driven quality improvements
|
||||||
- Better workflow and operator ergonomics
|
- Better workflow and operator ergonomics
|
||||||
|
|
||||||
### Package Notes
|
|
||||||
- npm package: \`ecc-universal\`
|
|
||||||
- Claude marketplace/plugin identifier: \`everything-claude-code@everything-claude-code\`
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
|
|||||||
5
.github/workflows/reusable-test.yml
vendored
5
.github/workflows/reusable-test.yml
vendored
@@ -130,10 +130,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
case "${{ inputs.package-manager }}" in
|
case "${{ inputs.package-manager }}" in
|
||||||
npm) npm ci ;;
|
npm) npm ci ;;
|
||||||
# pnpm v10 can fail CI on ignored native build scripts
|
pnpm) pnpm install --no-frozen-lockfile ;;
|
||||||
# (for example msgpackr-extract) even though this repo is Yarn-native
|
|
||||||
# and pnpm is only exercised here as a compatibility lane.
|
|
||||||
pnpm) pnpm install --config.strict-dep-builds=false --no-frozen-lockfile ;;
|
|
||||||
# Yarn Berry (v4+) removed --ignore-engines; engine checking is no longer a core feature
|
# Yarn Berry (v4+) removed --ignore-engines; engine checking is no longer a core feature
|
||||||
yarn) yarn install ;;
|
yarn) yarn install ;;
|
||||||
bun) bun install ;;
|
bun) bun install ;;
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -174,18 +174,10 @@ Get up and running in under 2 minutes:
|
|||||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||||
|
|
||||||
# Install plugin
|
# Install plugin
|
||||||
/plugin install everything-claude-code@everything-claude-code
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
### Naming + Migration Note
|
> Install-name clarification: older posts may still show `ecc@ecc`. That shorthand is deprecated. Anthropic marketplace/plugin installs are keyed by a canonical plugin identifier, so ECC standardized on `everything-claude-code@everything-claude-code` to keep the listing name, install path, `/plugin list`, and repo docs aligned instead of maintaining two different public names for the same plugin.
|
||||||
|
|
||||||
ECC now has three public identifiers, and they are not interchangeable:
|
|
||||||
|
|
||||||
- GitHub source repo: `affaan-m/everything-claude-code`
|
|
||||||
- Claude marketplace/plugin identifier: `everything-claude-code@everything-claude-code`
|
|
||||||
- npm package: `ecc-universal`
|
|
||||||
|
|
||||||
This is intentional. Anthropic marketplace/plugin installs are keyed by a canonical plugin identifier, so ECC standardized on `everything-claude-code@everything-claude-code` to keep the listing name, `/plugin install`, `/plugin list`, and repo docs aligned to one public install surface. Older posts may still show the old short-form nickname; that shorthand is deprecated. Separately, the npm package stayed on `ecc-universal`, so npm installs and marketplace installs intentionally use different names.
|
|
||||||
|
|
||||||
### Step 2: Install Rules (Required)
|
### Step 2: Install Rules (Required)
|
||||||
|
|
||||||
@@ -193,8 +185,6 @@ This is intentional. Anthropic marketplace/plugin installs are keyed by a canoni
|
|||||||
>
|
>
|
||||||
> If your local Claude setup was wiped or reset, that does not mean you need to repurchase ECC. Start with `ecc list-installed`, then run `ecc doctor` and `ecc repair` before reinstalling anything. That usually restores ECC-managed files without rebuilding your setup. If the problem is account or marketplace access for ECC Tools, handle billing/account recovery separately.
|
> If your local Claude setup was wiped or reset, that does not mean you need to repurchase ECC. Start with `ecc list-installed`, then run `ecc doctor` and `ecc repair` before reinstalling anything. That usually restores ECC-managed files without rebuilding your setup. If the problem is account or marketplace access for ECC Tools, handle billing/account recovery separately.
|
||||||
|
|
||||||
> If your local Claude setup was wiped or reset, that does not mean you need to repurchase ECC. Start with `ecc list-installed`, then run `ecc doctor` and `ecc repair` before reinstalling anything. That usually restores ECC-managed files without rebuilding your setup. If the problem is account or marketplace access for ECC Tools, handle billing/account recovery separately.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repo first
|
# Clone the repo first
|
||||||
git clone https://github.com/affaan-m/everything-claude-code.git
|
git clone https://github.com/affaan-m/everything-claude-code.git
|
||||||
@@ -660,7 +650,7 @@ The easiest way to use this repo - install as a Claude Code plugin:
|
|||||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||||
|
|
||||||
# Install the plugin
|
# Install the plugin
|
||||||
/plugin install everything-claude-code@everything-claude-code
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
Or add directly to your `~/.claude/settings.json`:
|
Or add directly to your `~/.claude/settings.json`:
|
||||||
|
|||||||
@@ -102,10 +102,10 @@
|
|||||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||||
|
|
||||||
# 安装插件
|
# 安装插件
|
||||||
/plugin install everything-claude-code@everything-claude-code
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
> 安装名称说明:较早的帖子里可能还会出现旧的短别名。那个旧缩写现在已经废弃。Anthropic 的 marketplace/plugin 安装是按规范化插件标识符寻址的,因此 ECC 统一为 `everything-claude-code@everything-claude-code`,这样市场条目、安装命令、`/plugin list` 输出和仓库文档都使用同一个公开名称,不再出现两个名字指向同一插件的混乱。
|
> 安装名称说明:较早的帖子里可能还会出现 `ecc@ecc`。那个旧缩写现在已经废弃。Anthropic 的 marketplace/plugin 安装是按规范化插件标识符寻址的,因此 ECC 统一为 `everything-claude-code@everything-claude-code`,这样市场条目、安装命令、`/plugin list` 输出和仓库文档都使用同一个公开名称,不再出现两个名字指向同一插件的混乱。
|
||||||
|
|
||||||
### 第二步:安装规则(必需)
|
### 第二步:安装规则(必需)
|
||||||
|
|
||||||
@@ -548,7 +548,7 @@ Claude Code v2.1+ 会**按照约定自动加载**已安装插件中的 `hooks/ho
|
|||||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||||
|
|
||||||
# 安装插件
|
# 安装插件
|
||||||
/plugin install everything-claude-code@everything-claude-code
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
或直接添加到你的 `~/.claude/settings.json`:
|
或直接添加到你的 `~/.claude/settings.json`:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Everything Claude Code プロジェクトのインタラクティブなステッ
|
|||||||
## 前提条件
|
## 前提条件
|
||||||
|
|
||||||
このスキルは起動前に Claude Code からアクセス可能である必要があります。ブートストラップには2つの方法があります:
|
このスキルは起動前に Claude Code からアクセス可能である必要があります。ブートストラップには2つの方法があります:
|
||||||
1. **プラグイン経由**: `/plugin install everything-claude-code@everything-claude-code` — プラグインがこのスキルを自動的にロードします
|
1. **プラグイン経由**: `/plugin install everything-claude-code` — プラグインがこのスキルを自動的にロードします
|
||||||
2. **手動**: このスキルのみを `~/.claude/skills/configure-ecc/SKILL.md` にコピーし、"configure ecc" と言って起動します
|
2. **手動**: このスキルのみを `~/.claude/skills/configure-ecc/SKILL.md` にコピーし、"configure ecc" と言って起動します
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ Comece em menos de 2 minutos:
|
|||||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||||
|
|
||||||
# Instalar plugin
|
# Instalar plugin
|
||||||
/plugin install everything-claude-code@everything-claude-code
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
### Passo 2: Instalar as Regras (Obrigatório)
|
### Passo 2: Instalar as Regras (Obrigatório)
|
||||||
@@ -313,7 +313,7 @@ claude --version
|
|||||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||||
|
|
||||||
# Instalar o plugin
|
# Instalar o plugin
|
||||||
/plugin install everything-claude-code@everything-claude-code
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
Ou adicione diretamente ao seu `~/.claude/settings.json`:
|
Ou adicione diretamente ao seu `~/.claude/settings.json`:
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ origin: ECC
|
|||||||
|
|
||||||
此技能必须在激活前对 Claude Code 可访问。有两种引导方式:
|
此技能必须在激活前对 Claude Code 可访问。有两种引导方式:
|
||||||
|
|
||||||
1. **通过插件**: `/plugin install everything-claude-code@everything-claude-code` — 插件会自动加载此技能
|
1. **通过插件**: `/plugin install everything-claude-code` — 插件会自动加载此技能
|
||||||
2. **手动**: 仅将此技能复制到 `~/.claude/skills/configure-ecc/SKILL.md`,然后通过说 "configure ecc" 激活
|
2. **手动**: 仅将此技能复制到 `~/.claude/skills/configure-ecc/SKILL.md`,然后通过说 "configure ecc" 激活
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|||||||
@@ -411,27 +411,6 @@ pub struct SessionMetrics {
|
|||||||
pub cost_usd: f64,
|
pub cost_usd: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
pub struct SessionBoardMeta {
|
|
||||||
pub lane: String,
|
|
||||||
pub project: Option<String>,
|
|
||||||
pub feature: Option<String>,
|
|
||||||
pub issue: Option<String>,
|
|
||||||
pub row_label: Option<String>,
|
|
||||||
pub previous_lane: Option<String>,
|
|
||||||
pub previous_row_label: Option<String>,
|
|
||||||
pub column_index: i64,
|
|
||||||
pub row_index: i64,
|
|
||||||
pub stack_index: i64,
|
|
||||||
pub progress_percent: i64,
|
|
||||||
pub status_detail: Option<String>,
|
|
||||||
pub movement_note: Option<String>,
|
|
||||||
pub activity_kind: Option<String>,
|
|
||||||
pub activity_note: Option<String>,
|
|
||||||
pub handoff_backlog: i64,
|
|
||||||
pub conflict_signal: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SessionMessage {
|
pub struct SessionMessage {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ use super::{
|
|||||||
ContextGraphObservation, ContextGraphRecallEntry, ContextGraphRelation, ContextGraphSyncStats,
|
ContextGraphObservation, ContextGraphRecallEntry, ContextGraphRelation, ContextGraphSyncStats,
|
||||||
ContextObservationPriority, DecisionLogEntry, FileActivityAction, FileActivityEntry,
|
ContextObservationPriority, DecisionLogEntry, FileActivityAction, FileActivityEntry,
|
||||||
HarnessKind, RemoteDispatchKind, RemoteDispatchRequest, RemoteDispatchStatus, ScheduledTask,
|
HarnessKind, RemoteDispatchKind, RemoteDispatchRequest, RemoteDispatchStatus, ScheduledTask,
|
||||||
Session, SessionAgentProfile, SessionBoardMeta, SessionHarnessInfo, SessionMessage,
|
Session, SessionAgentProfile, SessionHarnessInfo, SessionMessage, SessionMetrics, SessionState,
|
||||||
SessionMetrics, SessionState, WorktreeInfo,
|
WorktreeInfo,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct StateStore {
|
pub struct StateStore {
|
||||||
@@ -241,28 +241,6 @@ impl StateStore {
|
|||||||
timestamp TEXT NOT NULL
|
timestamp TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS session_board (
|
|
||||||
session_id TEXT PRIMARY KEY REFERENCES sessions(id) ON DELETE CASCADE,
|
|
||||||
lane TEXT NOT NULL,
|
|
||||||
project TEXT,
|
|
||||||
feature TEXT,
|
|
||||||
issue TEXT,
|
|
||||||
row_label TEXT,
|
|
||||||
previous_lane TEXT,
|
|
||||||
previous_row_label TEXT,
|
|
||||||
column_index INTEGER NOT NULL DEFAULT 0,
|
|
||||||
row_index INTEGER NOT NULL DEFAULT 0,
|
|
||||||
stack_index INTEGER NOT NULL DEFAULT 0,
|
|
||||||
progress_percent INTEGER NOT NULL DEFAULT 0,
|
|
||||||
status_detail TEXT,
|
|
||||||
movement_note TEXT,
|
|
||||||
activity_kind TEXT,
|
|
||||||
activity_note TEXT,
|
|
||||||
handoff_backlog INTEGER NOT NULL DEFAULT 0,
|
|
||||||
conflict_signal TEXT,
|
|
||||||
updated_at TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS decision_log (
|
CREATE TABLE IF NOT EXISTS decision_log (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
||||||
@@ -408,9 +386,6 @@ impl StateStore {
|
|||||||
CREATE INDEX IF NOT EXISTS idx_messages_to ON messages(to_session, read);
|
CREATE INDEX IF NOT EXISTS idx_messages_to ON messages(to_session, read);
|
||||||
CREATE INDEX IF NOT EXISTS idx_session_output_session
|
CREATE INDEX IF NOT EXISTS idx_session_output_session
|
||||||
ON session_output(session_id, id);
|
ON session_output(session_id, id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_session_board_lane ON session_board(lane);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_session_board_coords
|
|
||||||
ON session_board(column_index, row_index, stack_index);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_decision_log_session
|
CREATE INDEX IF NOT EXISTS idx_decision_log_session
|
||||||
ON decision_log(session_id, timestamp, id);
|
ON decision_log(session_id, timestamp, id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_context_graph_entities_session
|
CREATE INDEX IF NOT EXISTS idx_context_graph_entities_session
|
||||||
@@ -434,8 +409,6 @@ impl StateStore {
|
|||||||
",
|
",
|
||||||
)?;
|
)?;
|
||||||
self.ensure_session_columns()?;
|
self.ensure_session_columns()?;
|
||||||
self.ensure_session_board_columns()?;
|
|
||||||
self.refresh_session_board_meta()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,51 +482,6 @@ impl StateStore {
|
|||||||
.context("Failed to add output_tokens column to sessions table")?;
|
.context("Failed to add output_tokens column to sessions table")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.has_column("sessions", "tokens_used")? {
|
|
||||||
self.conn
|
|
||||||
.execute(
|
|
||||||
"ALTER TABLE sessions ADD COLUMN tokens_used INTEGER NOT NULL DEFAULT 0",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.context("Failed to add tokens_used column to sessions table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("sessions", "tool_calls")? {
|
|
||||||
self.conn
|
|
||||||
.execute(
|
|
||||||
"ALTER TABLE sessions ADD COLUMN tool_calls INTEGER NOT NULL DEFAULT 0",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.context("Failed to add tool_calls column to sessions table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("sessions", "files_changed")? {
|
|
||||||
self.conn
|
|
||||||
.execute(
|
|
||||||
"ALTER TABLE sessions ADD COLUMN files_changed INTEGER NOT NULL DEFAULT 0",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.context("Failed to add files_changed column to sessions table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("sessions", "duration_secs")? {
|
|
||||||
self.conn
|
|
||||||
.execute(
|
|
||||||
"ALTER TABLE sessions ADD COLUMN duration_secs INTEGER NOT NULL DEFAULT 0",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.context("Failed to add duration_secs column to sessions table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("sessions", "cost_usd")? {
|
|
||||||
self.conn
|
|
||||||
.execute(
|
|
||||||
"ALTER TABLE sessions ADD COLUMN cost_usd REAL NOT NULL DEFAULT 0.0",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.context("Failed to add cost_usd column to sessions table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("sessions", "last_heartbeat_at")? {
|
if !self.has_column("sessions", "last_heartbeat_at")? {
|
||||||
self.conn
|
self.conn
|
||||||
.execute("ALTER TABLE sessions ADD COLUMN last_heartbeat_at TEXT", [])
|
.execute("ALTER TABLE sessions ADD COLUMN last_heartbeat_at TEXT", [])
|
||||||
@@ -568,24 +496,6 @@ impl StateStore {
|
|||||||
.context("Failed to backfill last_heartbeat_at column")?;
|
.context("Failed to backfill last_heartbeat_at column")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.has_column("sessions", "worktree_path")? {
|
|
||||||
self.conn
|
|
||||||
.execute("ALTER TABLE sessions ADD COLUMN worktree_path TEXT", [])
|
|
||||||
.context("Failed to add worktree_path column to sessions table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("sessions", "worktree_branch")? {
|
|
||||||
self.conn
|
|
||||||
.execute("ALTER TABLE sessions ADD COLUMN worktree_branch TEXT", [])
|
|
||||||
.context("Failed to add worktree_branch column to sessions table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("sessions", "worktree_base")? {
|
|
||||||
self.conn
|
|
||||||
.execute("ALTER TABLE sessions ADD COLUMN worktree_base TEXT", [])
|
|
||||||
.context("Failed to add worktree_base column to sessions table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("tool_log", "hook_event_id")? {
|
if !self.has_column("tool_log", "hook_event_id")? {
|
||||||
self.conn
|
self.conn
|
||||||
.execute("ALTER TABLE tool_log ADD COLUMN hook_event_id TEXT", [])
|
.execute("ALTER TABLE tool_log ADD COLUMN hook_event_id TEXT", [])
|
||||||
@@ -802,103 +712,6 @@ impl StateStore {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_session_board_columns(&self) -> Result<()> {
|
|
||||||
if !self.has_column("session_board", "row_label")? {
|
|
||||||
self.conn
|
|
||||||
.execute("ALTER TABLE session_board ADD COLUMN row_label TEXT", [])
|
|
||||||
.context("Failed to add row_label column to session_board table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("session_board", "previous_lane")? {
|
|
||||||
self.conn
|
|
||||||
.execute("ALTER TABLE session_board ADD COLUMN previous_lane TEXT", [])
|
|
||||||
.context("Failed to add previous_lane column to session_board table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("session_board", "previous_row_label")? {
|
|
||||||
self.conn
|
|
||||||
.execute("ALTER TABLE session_board ADD COLUMN previous_row_label TEXT", [])
|
|
||||||
.context("Failed to add previous_row_label column to session_board table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("session_board", "column_index")? {
|
|
||||||
self.conn
|
|
||||||
.execute(
|
|
||||||
"ALTER TABLE session_board ADD COLUMN column_index INTEGER NOT NULL DEFAULT 0",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.context("Failed to add column_index column to session_board table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("session_board", "row_index")? {
|
|
||||||
self.conn
|
|
||||||
.execute(
|
|
||||||
"ALTER TABLE session_board ADD COLUMN row_index INTEGER NOT NULL DEFAULT 0",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.context("Failed to add row_index column to session_board table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("session_board", "stack_index")? {
|
|
||||||
self.conn
|
|
||||||
.execute(
|
|
||||||
"ALTER TABLE session_board ADD COLUMN stack_index INTEGER NOT NULL DEFAULT 0",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.context("Failed to add stack_index column to session_board table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("session_board", "progress_percent")? {
|
|
||||||
self.conn
|
|
||||||
.execute(
|
|
||||||
"ALTER TABLE session_board ADD COLUMN progress_percent INTEGER NOT NULL DEFAULT 0",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.context("Failed to add progress_percent column to session_board table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("session_board", "status_detail")? {
|
|
||||||
self.conn
|
|
||||||
.execute("ALTER TABLE session_board ADD COLUMN status_detail TEXT", [])
|
|
||||||
.context("Failed to add status_detail column to session_board table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("session_board", "movement_note")? {
|
|
||||||
self.conn
|
|
||||||
.execute("ALTER TABLE session_board ADD COLUMN movement_note TEXT", [])
|
|
||||||
.context("Failed to add movement_note column to session_board table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("session_board", "activity_kind")? {
|
|
||||||
self.conn
|
|
||||||
.execute("ALTER TABLE session_board ADD COLUMN activity_kind TEXT", [])
|
|
||||||
.context("Failed to add activity_kind column to session_board table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("session_board", "activity_note")? {
|
|
||||||
self.conn
|
|
||||||
.execute("ALTER TABLE session_board ADD COLUMN activity_note TEXT", [])
|
|
||||||
.context("Failed to add activity_note column to session_board table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("session_board", "handoff_backlog")? {
|
|
||||||
self.conn
|
|
||||||
.execute(
|
|
||||||
"ALTER TABLE session_board ADD COLUMN handoff_backlog INTEGER NOT NULL DEFAULT 0",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
.context("Failed to add handoff_backlog column to session_board table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.has_column("session_board", "conflict_signal")? {
|
|
||||||
self.conn
|
|
||||||
.execute("ALTER TABLE session_board ADD COLUMN conflict_signal TEXT", [])
|
|
||||||
.context("Failed to add conflict_signal column to session_board table")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_column(&self, table: &str, column: &str) -> Result<bool> {
|
fn has_column(&self, table: &str, column: &str) -> Result<bool> {
|
||||||
let pragma = format!("PRAGMA table_info({table})");
|
let pragma = format!("PRAGMA table_info({table})");
|
||||||
let mut stmt = self.conn.prepare(&pragma)?;
|
let mut stmt = self.conn.prepare(&pragma)?;
|
||||||
@@ -976,7 +789,6 @@ impl StateStore {
|
|||||||
session.last_heartbeat_at.to_rfc3339(),
|
session.last_heartbeat_at.to_rfc3339(),
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
self.refresh_session_board_meta()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1097,7 +909,6 @@ impl StateStore {
|
|||||||
anyhow::bail!("Session not found: {session_id}");
|
anyhow::bail!("Session not found: {session_id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
self.refresh_session_board_meta()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1138,7 +949,6 @@ impl StateStore {
|
|||||||
anyhow::bail!("Session not found: {session_id}");
|
anyhow::bail!("Session not found: {session_id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
self.refresh_session_board_meta()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1160,7 +970,6 @@ impl StateStore {
|
|||||||
anyhow::bail!("Session not found: {session_id}");
|
anyhow::bail!("Session not found: {session_id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
self.refresh_session_board_meta()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1194,7 +1003,6 @@ impl StateStore {
|
|||||||
anyhow::bail!("Session not found: {session_id}");
|
anyhow::bail!("Session not found: {session_id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
self.refresh_session_board_meta()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1222,7 +1030,6 @@ impl StateStore {
|
|||||||
anyhow::bail!("Session not found: {session_id}");
|
anyhow::bail!("Session not found: {session_id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
self.refresh_session_board_meta()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1579,7 +1386,6 @@ impl StateStore {
|
|||||||
session_id,
|
session_id,
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
self.refresh_session_board_meta()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1631,7 +1437,6 @@ impl StateStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.refresh_session_board_meta()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1717,7 +1522,6 @@ impl StateStore {
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.refresh_session_board_meta()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2072,7 +1876,6 @@ impl StateStore {
|
|||||||
WHERE id = ?2",
|
WHERE id = ?2",
|
||||||
rusqlite::params![chrono::Utc::now().to_rfc3339(), session_id],
|
rusqlite::params![chrono::Utc::now().to_rfc3339(), session_id],
|
||||||
)?;
|
)?;
|
||||||
self.refresh_session_board_meta()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2176,46 +1979,6 @@ impl StateStore {
|
|||||||
Ok(harnesses)
|
Ok(harnesses)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_session_board_meta(&self) -> Result<HashMap<String, SessionBoardMeta>> {
|
|
||||||
let mut stmt = self.conn.prepare(
|
|
||||||
"SELECT session_id, lane, project, feature, issue, row_label,
|
|
||||||
previous_lane, previous_row_label,
|
|
||||||
column_index, row_index, stack_index, progress_percent,
|
|
||||||
status_detail, movement_note, activity_kind, activity_note,
|
|
||||||
handoff_backlog, conflict_signal
|
|
||||||
FROM session_board",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let meta = stmt
|
|
||||||
.query_map([], |row| {
|
|
||||||
Ok((
|
|
||||||
row.get::<_, String>(0)?,
|
|
||||||
SessionBoardMeta {
|
|
||||||
lane: row.get(1)?,
|
|
||||||
project: row.get(2)?,
|
|
||||||
feature: row.get(3)?,
|
|
||||||
issue: row.get(4)?,
|
|
||||||
row_label: row.get(5)?,
|
|
||||||
previous_lane: row.get(6)?,
|
|
||||||
previous_row_label: row.get(7)?,
|
|
||||||
column_index: row.get(8)?,
|
|
||||||
row_index: row.get(9)?,
|
|
||||||
stack_index: row.get(10)?,
|
|
||||||
progress_percent: row.get(11)?,
|
|
||||||
status_detail: row.get(12)?,
|
|
||||||
movement_note: row.get(13)?,
|
|
||||||
activity_kind: row.get(14)?,
|
|
||||||
activity_note: row.get(15)?,
|
|
||||||
handoff_backlog: row.get(16)?,
|
|
||||||
conflict_signal: row.get(17)?,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
})?
|
|
||||||
.collect::<Result<HashMap<_, _>, _>>()?;
|
|
||||||
|
|
||||||
Ok(meta)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_session_harness_info(&self, session_id: &str) -> Result<Option<SessionHarnessInfo>> {
|
pub fn get_session_harness_info(&self, session_id: &str) -> Result<Option<SessionHarnessInfo>> {
|
||||||
let mut stmt = self.conn.prepare(
|
let mut stmt = self.conn.prepare(
|
||||||
"SELECT harness, detected_harnesses_json, agent_type, working_dir
|
"SELECT harness, detected_harnesses_json, agent_type, working_dir
|
||||||
@@ -2245,94 +2008,6 @@ impl StateStore {
|
|||||||
Ok(self.list_sessions()?.into_iter().next())
|
Ok(self.list_sessions()?.into_iter().next())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn refresh_session_board_meta(&self) -> Result<()> {
|
|
||||||
self.conn.execute(
|
|
||||||
"DELETE FROM session_board
|
|
||||||
WHERE session_id NOT IN (SELECT id FROM sessions)",
|
|
||||||
[],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let existing_meta = self.list_session_board_meta().unwrap_or_default();
|
|
||||||
let sessions = self.list_sessions()?;
|
|
||||||
let board_meta = derive_board_meta_map(&sessions);
|
|
||||||
let now = chrono::Utc::now().to_rfc3339();
|
|
||||||
|
|
||||||
for session in sessions {
|
|
||||||
let mut meta = board_meta
|
|
||||||
.get(&session.id)
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| SessionBoardMeta {
|
|
||||||
lane: board_lane_for_state(&session.state).to_string(),
|
|
||||||
..SessionBoardMeta::default()
|
|
||||||
});
|
|
||||||
if let Some(previous) = existing_meta.get(&session.id) {
|
|
||||||
annotate_board_motion(&mut meta, previous);
|
|
||||||
}
|
|
||||||
if let Some((activity_kind, activity_note)) =
|
|
||||||
self.latest_task_handoff_activity(&session.id)?
|
|
||||||
{
|
|
||||||
meta.activity_kind = Some(activity_kind);
|
|
||||||
meta.activity_note = Some(activity_note);
|
|
||||||
} else {
|
|
||||||
meta.activity_kind = None;
|
|
||||||
meta.activity_note = None;
|
|
||||||
}
|
|
||||||
meta.handoff_backlog = self.unread_task_handoff_count(&session.id)? as i64;
|
|
||||||
|
|
||||||
self.conn.execute(
|
|
||||||
"INSERT INTO session_board (
|
|
||||||
session_id, lane, project, feature, issue, row_label,
|
|
||||||
previous_lane, previous_row_label,
|
|
||||||
column_index, row_index, stack_index, progress_percent,
|
|
||||||
status_detail, movement_note, activity_kind, activity_note,
|
|
||||||
handoff_backlog, conflict_signal, updated_at
|
|
||||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19)
|
|
||||||
ON CONFLICT(session_id) DO UPDATE SET
|
|
||||||
lane = excluded.lane,
|
|
||||||
project = excluded.project,
|
|
||||||
feature = excluded.feature,
|
|
||||||
issue = excluded.issue,
|
|
||||||
row_label = excluded.row_label,
|
|
||||||
previous_lane = excluded.previous_lane,
|
|
||||||
previous_row_label = excluded.previous_row_label,
|
|
||||||
column_index = excluded.column_index,
|
|
||||||
row_index = excluded.row_index,
|
|
||||||
stack_index = excluded.stack_index,
|
|
||||||
progress_percent = excluded.progress_percent,
|
|
||||||
status_detail = excluded.status_detail,
|
|
||||||
movement_note = excluded.movement_note,
|
|
||||||
activity_kind = excluded.activity_kind,
|
|
||||||
activity_note = excluded.activity_note,
|
|
||||||
handoff_backlog = excluded.handoff_backlog,
|
|
||||||
conflict_signal = excluded.conflict_signal,
|
|
||||||
updated_at = excluded.updated_at",
|
|
||||||
rusqlite::params![
|
|
||||||
session.id,
|
|
||||||
meta.lane,
|
|
||||||
meta.project,
|
|
||||||
meta.feature,
|
|
||||||
meta.issue,
|
|
||||||
meta.row_label,
|
|
||||||
meta.previous_lane,
|
|
||||||
meta.previous_row_label,
|
|
||||||
meta.column_index,
|
|
||||||
meta.row_index,
|
|
||||||
meta.stack_index,
|
|
||||||
meta.progress_percent,
|
|
||||||
meta.status_detail,
|
|
||||||
meta.movement_note,
|
|
||||||
meta.activity_kind,
|
|
||||||
meta.activity_note,
|
|
||||||
meta.handoff_backlog,
|
|
||||||
meta.conflict_signal,
|
|
||||||
now,
|
|
||||||
],
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_session(&self, id: &str) -> Result<Option<Session>> {
|
pub fn get_session(&self, id: &str) -> Result<Option<Session>> {
|
||||||
let sessions = self.list_sessions()?;
|
let sessions = self.list_sessions()?;
|
||||||
Ok(sessions
|
Ok(sessions
|
||||||
@@ -2363,7 +2038,6 @@ impl StateStore {
|
|||||||
anyhow::bail!("Session not found: {session_id}");
|
anyhow::bail!("Session not found: {session_id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
self.refresh_session_board_meta()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2374,7 +2048,6 @@ impl StateStore {
|
|||||||
rusqlite::params![from, to, content, msg_type, chrono::Utc::now().to_rfc3339()],
|
rusqlite::params![from, to, content, msg_type, chrono::Utc::now().to_rfc3339()],
|
||||||
)?;
|
)?;
|
||||||
self.sync_context_graph_message(from, to, content, msg_type)?;
|
self.sync_context_graph_message(from, to, content, msg_type)?;
|
||||||
self.refresh_session_board_meta()?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2645,7 +2318,6 @@ impl StateStore {
|
|||||||
rusqlite::params![session_id],
|
rusqlite::params![session_id],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
self.refresh_session_board_meta()?;
|
|
||||||
Ok(updated)
|
Ok(updated)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2655,7 +2327,6 @@ impl StateStore {
|
|||||||
rusqlite::params![message_id],
|
rusqlite::params![message_id],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
self.refresh_session_board_meta()?;
|
|
||||||
Ok(updated)
|
Ok(updated)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2674,75 +2345,6 @@ impl StateStore {
|
|||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn latest_task_handoff_activity(
|
|
||||||
&self,
|
|
||||||
session_id: &str,
|
|
||||||
) -> Result<Option<(String, String)>> {
|
|
||||||
let latest_handoff = self
|
|
||||||
.conn
|
|
||||||
.query_row(
|
|
||||||
"SELECT from_session, to_session, content
|
|
||||||
FROM messages
|
|
||||||
WHERE msg_type = 'task_handoff'
|
|
||||||
AND (from_session = ?1 OR to_session = ?1)
|
|
||||||
ORDER BY id DESC
|
|
||||||
LIMIT 1",
|
|
||||||
rusqlite::params![session_id],
|
|
||||||
|row| {
|
|
||||||
Ok((
|
|
||||||
row.get::<_, String>(0)?,
|
|
||||||
row.get::<_, String>(1)?,
|
|
||||||
row.get::<_, String>(2)?,
|
|
||||||
))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.optional()?;
|
|
||||||
|
|
||||||
Ok(latest_handoff.and_then(|(from_session, to_session, content)| {
|
|
||||||
let context = extract_task_handoff_context(&content)?;
|
|
||||||
let routing_suffix = routing_activity_suffix(&context);
|
|
||||||
|
|
||||||
if session_id == to_session {
|
|
||||||
Some((
|
|
||||||
"received".to_string(),
|
|
||||||
format!(
|
|
||||||
"Received from {}{}",
|
|
||||||
short_session_ref(&from_session),
|
|
||||||
routing_suffix
|
|
||||||
.map(|value| format!(" | {value}"))
|
|
||||||
.unwrap_or_default()
|
|
||||||
),
|
|
||||||
))
|
|
||||||
} else if session_id == from_session {
|
|
||||||
let (kind, base) = match routing_suffix {
|
|
||||||
Some("spawned") => {
|
|
||||||
("spawned", format!("Spawned {}", short_session_ref(&to_session)))
|
|
||||||
}
|
|
||||||
Some("spawned fallback") => (
|
|
||||||
"spawned_fallback",
|
|
||||||
format!("Spawned fallback {}", short_session_ref(&to_session)),
|
|
||||||
),
|
|
||||||
_ => (
|
|
||||||
"delegated",
|
|
||||||
format!("Delegated to {}", short_session_ref(&to_session)),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
Some((
|
|
||||||
kind.to_string(),
|
|
||||||
format!(
|
|
||||||
"{base}{}",
|
|
||||||
routing_suffix
|
|
||||||
.filter(|value| !value.starts_with("spawned"))
|
|
||||||
.map(|value| format!(" | {value}"))
|
|
||||||
.unwrap_or_default()
|
|
||||||
),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert_decision(
|
pub fn insert_decision(
|
||||||
&self,
|
&self,
|
||||||
session_id: &str,
|
session_id: &str,
|
||||||
@@ -4260,411 +3862,6 @@ fn file_activity_action_value(action: &FileActivityAction) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn board_lane_for_state(state: &SessionState) -> &'static str {
|
|
||||||
match state {
|
|
||||||
SessionState::Pending => "Inbox",
|
|
||||||
SessionState::Running => "In Progress",
|
|
||||||
SessionState::Idle => "Review",
|
|
||||||
SessionState::Stale | SessionState::Failed => "Blocked",
|
|
||||||
SessionState::Completed => "Done",
|
|
||||||
SessionState::Stopped => "Stopped",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn derive_board_scope(session: &Session) -> (Option<String>, Option<String>, Option<String>) {
|
|
||||||
let project = extract_labeled_scope(&session.task, &["project", "roadmap", "epic"]);
|
|
||||||
let feature = extract_labeled_scope(&session.task, &["feature", "workflow", "flow"]);
|
|
||||||
let issue = extract_issue_reference(&session.task);
|
|
||||||
(project, feature, issue)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn derive_board_meta_map(sessions: &[Session]) -> HashMap<String, SessionBoardMeta> {
|
|
||||||
let conflict_signals = derive_board_conflict_signals(sessions);
|
|
||||||
let scopes = sessions
|
|
||||||
.iter()
|
|
||||||
.map(|session| (session.id.clone(), derive_board_scope(session)))
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
|
|
||||||
let mut row_specs = scopes
|
|
||||||
.iter()
|
|
||||||
.map(|(session_id, (project, feature, issue))| {
|
|
||||||
let row_label = issue
|
|
||||||
.clone()
|
|
||||||
.or_else(|| feature.clone())
|
|
||||||
.or_else(|| project.clone())
|
|
||||||
.or_else(|| {
|
|
||||||
sessions
|
|
||||||
.iter()
|
|
||||||
.find(|session| &session.id == session_id)
|
|
||||||
.and_then(|session| session.worktree.as_ref())
|
|
||||||
.map(|worktree| worktree.branch.clone())
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| "General".to_string());
|
|
||||||
|
|
||||||
let row_rank = if issue.is_some() {
|
|
||||||
0
|
|
||||||
} else if feature.is_some() {
|
|
||||||
1
|
|
||||||
} else if project.is_some() {
|
|
||||||
2
|
|
||||||
} else {
|
|
||||||
3
|
|
||||||
};
|
|
||||||
|
|
||||||
(session_id.clone(), row_label, row_rank)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
row_specs.sort_by(|left, right| {
|
|
||||||
left.2
|
|
||||||
.cmp(&right.2)
|
|
||||||
.then_with(|| left.1.to_ascii_lowercase().cmp(&right.1.to_ascii_lowercase()))
|
|
||||||
.then_with(|| left.0.cmp(&right.0))
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut row_indices = HashMap::new();
|
|
||||||
let mut next_row_index = 0_i64;
|
|
||||||
for (_, row_label, row_rank) in &row_specs {
|
|
||||||
let key = (*row_rank, row_label.clone());
|
|
||||||
if let std::collections::hash_map::Entry::Vacant(entry) = row_indices.entry(key) {
|
|
||||||
entry.insert(next_row_index);
|
|
||||||
next_row_index += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut stack_counts: HashMap<(i64, i64), i64> = HashMap::new();
|
|
||||||
let mut board_meta = HashMap::new();
|
|
||||||
|
|
||||||
for session in sessions {
|
|
||||||
let (project, feature, issue) = scopes
|
|
||||||
.get(&session.id)
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or((None, None, None));
|
|
||||||
let (_, row_label, row_rank) = row_specs
|
|
||||||
.iter()
|
|
||||||
.find(|(session_id, _, _)| session_id == &session.id)
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| (session.id.clone(), "General".to_string(), 4));
|
|
||||||
let column_index = board_column_index(&session.state);
|
|
||||||
let row_index = row_indices
|
|
||||||
.get(&(row_rank, row_label.clone()))
|
|
||||||
.copied()
|
|
||||||
.unwrap_or_default();
|
|
||||||
let stack_index = {
|
|
||||||
let entry = stack_counts.entry((column_index, row_index)).or_insert(0);
|
|
||||||
let current = *entry;
|
|
||||||
*entry += 1;
|
|
||||||
current
|
|
||||||
};
|
|
||||||
|
|
||||||
board_meta.insert(
|
|
||||||
session.id.clone(),
|
|
||||||
SessionBoardMeta {
|
|
||||||
lane: board_lane_for_state(&session.state).to_string(),
|
|
||||||
project,
|
|
||||||
feature,
|
|
||||||
issue,
|
|
||||||
row_label: Some(row_label),
|
|
||||||
previous_lane: None,
|
|
||||||
previous_row_label: None,
|
|
||||||
column_index,
|
|
||||||
row_index,
|
|
||||||
stack_index,
|
|
||||||
progress_percent: derive_board_progress_percent(session),
|
|
||||||
status_detail: derive_board_status_detail(session),
|
|
||||||
movement_note: None,
|
|
||||||
activity_kind: None,
|
|
||||||
activity_note: None,
|
|
||||||
handoff_backlog: 0,
|
|
||||||
conflict_signal: conflict_signals.get(&session.id).cloned(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
board_meta
|
|
||||||
}
|
|
||||||
|
|
||||||
fn board_column_index(state: &SessionState) -> i64 {
|
|
||||||
match state {
|
|
||||||
SessionState::Pending => 0,
|
|
||||||
SessionState::Running => 1,
|
|
||||||
SessionState::Idle => 2,
|
|
||||||
SessionState::Stale | SessionState::Failed => 3,
|
|
||||||
SessionState::Completed => 4,
|
|
||||||
SessionState::Stopped => 5,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn derive_board_progress_percent(session: &Session) -> i64 {
|
|
||||||
match session.state {
|
|
||||||
SessionState::Pending => 10,
|
|
||||||
SessionState::Running => {
|
|
||||||
if session.metrics.files_changed > 0 {
|
|
||||||
60
|
|
||||||
} else if session.worktree.is_some() || session.metrics.tool_calls > 0 {
|
|
||||||
45
|
|
||||||
} else {
|
|
||||||
25
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SessionState::Idle => 85,
|
|
||||||
SessionState::Stale => 55,
|
|
||||||
SessionState::Completed => 100,
|
|
||||||
SessionState::Failed => 65,
|
|
||||||
SessionState::Stopped => 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn derive_board_status_detail(session: &Session) -> Option<String> {
|
|
||||||
let detail = match session.state {
|
|
||||||
SessionState::Pending => "Queued",
|
|
||||||
SessionState::Running => {
|
|
||||||
if session.metrics.files_changed > 0 {
|
|
||||||
"Actively editing"
|
|
||||||
} else if session.worktree.is_some() {
|
|
||||||
"Scoping"
|
|
||||||
} else {
|
|
||||||
"Booting"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SessionState::Idle => "Awaiting review",
|
|
||||||
SessionState::Stale => "Needs heartbeat",
|
|
||||||
SessionState::Completed => "Task complete",
|
|
||||||
SessionState::Failed => "Blocked by failure",
|
|
||||||
SessionState::Stopped => "Stopped",
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(detail.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn annotate_board_motion(current: &mut SessionBoardMeta, previous: &SessionBoardMeta) {
|
|
||||||
if previous.lane != current.lane {
|
|
||||||
current.previous_lane = Some(previous.lane.clone());
|
|
||||||
current.previous_row_label = previous.row_label.clone();
|
|
||||||
current.movement_note = Some(match current.lane.as_str() {
|
|
||||||
"Blocked" => "Blocked".to_string(),
|
|
||||||
"Done" => "Completed".to_string(),
|
|
||||||
_ => format!("Moved {} -> {}", previous.lane, current.lane),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if previous.row_label != current.row_label {
|
|
||||||
let from = previous
|
|
||||||
.row_label
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| "General".to_string());
|
|
||||||
let to = current
|
|
||||||
.row_label
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| "General".to_string());
|
|
||||||
current.previous_lane = Some(previous.lane.clone());
|
|
||||||
current.previous_row_label = previous.row_label.clone();
|
|
||||||
current.movement_note = Some(format!("Retargeted {from} -> {to}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_labeled_scope(task: &str, labels: &[&str]) -> Option<String> {
|
|
||||||
let lowered = task.to_ascii_lowercase();
|
|
||||||
|
|
||||||
for label in labels {
|
|
||||||
if let Some(index) = lowered.find(label) {
|
|
||||||
let mut tail = task.get(index + label.len()..)?.trim_start_matches([' ', ':', '-', '#']);
|
|
||||||
if tail.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((candidate, _)) = tail
|
|
||||||
.split_once('|')
|
|
||||||
.or_else(|| tail.split_once(';'))
|
|
||||||
.or_else(|| tail.split_once(','))
|
|
||||||
.or_else(|| tail.split_once('\n'))
|
|
||||||
{
|
|
||||||
tail = candidate;
|
|
||||||
}
|
|
||||||
|
|
||||||
let words = tail
|
|
||||||
.split_whitespace()
|
|
||||||
.take(4)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" ")
|
|
||||||
.trim()
|
|
||||||
.trim_matches(|ch: char| matches!(ch, '.' | ',' | ';' | ':' | '|'))
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
if !words.is_empty() {
|
|
||||||
return Some(words);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_issue_reference(task: &str) -> Option<String> {
|
|
||||||
let tokens = task
|
|
||||||
.split(|ch: char| ch.is_whitespace() || matches!(ch, ',' | ';' | ':' | '(' | ')'))
|
|
||||||
.filter(|token| !token.is_empty());
|
|
||||||
|
|
||||||
for token in tokens {
|
|
||||||
if let Some(stripped) = token.strip_prefix('#') {
|
|
||||||
if !stripped.is_empty() && stripped.chars().all(|ch| ch.is_ascii_digit()) {
|
|
||||||
return Some(format!("#{stripped}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((prefix, suffix)) = token.split_once('-') {
|
|
||||||
if !prefix.is_empty()
|
|
||||||
&& !suffix.is_empty()
|
|
||||||
&& prefix.chars().all(|ch| ch.is_ascii_uppercase())
|
|
||||||
&& suffix.chars().all(|ch| ch.is_ascii_digit())
|
|
||||||
{
|
|
||||||
return Some(token.trim_matches('.').to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn derive_board_conflict_signals(sessions: &[Session]) -> HashMap<String, String> {
|
|
||||||
let active_sessions = sessions
|
|
||||||
.iter()
|
|
||||||
.filter(|session| {
|
|
||||||
matches!(
|
|
||||||
session.state,
|
|
||||||
SessionState::Pending | SessionState::Running | SessionState::Idle | SessionState::Stale
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let mut sessions_by_branch: HashMap<String, Vec<&Session>> = HashMap::new();
|
|
||||||
let mut sessions_by_task: HashMap<String, Vec<&Session>> = HashMap::new();
|
|
||||||
let mut sessions_by_scope: HashMap<String, Vec<&Session>> = HashMap::new();
|
|
||||||
|
|
||||||
for session in active_sessions {
|
|
||||||
if let Some(worktree) = session.worktree.as_ref() {
|
|
||||||
sessions_by_branch
|
|
||||||
.entry(worktree.branch.clone())
|
|
||||||
.or_default()
|
|
||||||
.push(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
sessions_by_task
|
|
||||||
.entry(session.task.trim().to_ascii_lowercase())
|
|
||||||
.or_default()
|
|
||||||
.push(session);
|
|
||||||
|
|
||||||
let (project, feature, issue) = derive_board_scope(session);
|
|
||||||
if let Some(scope) = issue.or(feature).or(project).filter(|scope| !scope.is_empty()) {
|
|
||||||
sessions_by_scope.entry(scope).or_default().push(session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut signals = HashMap::new();
|
|
||||||
|
|
||||||
for (branch, grouped_sessions) in sessions_by_branch {
|
|
||||||
if grouped_sessions.len() < 2 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for session in grouped_sessions {
|
|
||||||
append_conflict_signal(&mut signals, &session.id, format!("Shared branch {branch}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (task, grouped_sessions) in sessions_by_task {
|
|
||||||
if grouped_sessions.len() < 2 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for session in grouped_sessions {
|
|
||||||
append_conflict_signal(
|
|
||||||
&mut signals,
|
|
||||||
&session.id,
|
|
||||||
format!("Shared task {}", truncate_task_for_signal(&task)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (scope, grouped_sessions) in sessions_by_scope {
|
|
||||||
if grouped_sessions.len() < 2 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for session in grouped_sessions {
|
|
||||||
append_conflict_signal(
|
|
||||||
&mut signals,
|
|
||||||
&session.id,
|
|
||||||
format!("Shared scope {}", truncate_task_for_signal(&scope)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
signals
|
|
||||||
}
|
|
||||||
|
|
||||||
fn append_conflict_signal(
|
|
||||||
signals: &mut HashMap<String, String>,
|
|
||||||
session_id: &str,
|
|
||||||
next_signal: String,
|
|
||||||
) {
|
|
||||||
let entry = signals.entry(session_id.to_string()).or_default();
|
|
||||||
if entry.is_empty() {
|
|
||||||
*entry = next_signal;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !entry.split("; ").any(|existing| existing == next_signal) {
|
|
||||||
entry.push_str("; ");
|
|
||||||
entry.push_str(&next_signal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn short_session_ref(session_id: &str) -> String {
|
|
||||||
if session_id.chars().count() <= 12 {
|
|
||||||
session_id.to_string()
|
|
||||||
} else {
|
|
||||||
session_id.chars().take(8).collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn routing_activity_suffix(context: &str) -> Option<&'static str> {
|
|
||||||
let normalized = context.to_ascii_lowercase();
|
|
||||||
if normalized.contains("reused idle delegate") {
|
|
||||||
Some("reused idle")
|
|
||||||
} else if normalized.contains("reused active delegate") {
|
|
||||||
Some("reused active")
|
|
||||||
} else if normalized.contains("spawned fallback delegate") {
|
|
||||||
Some("spawned fallback")
|
|
||||||
} else if normalized.contains("spawned new delegate") {
|
|
||||||
Some("spawned")
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_task_handoff_context(content: &str) -> Option<String> {
|
|
||||||
if let Some(crate::comms::MessageType::TaskHandoff { context, .. }) = crate::comms::parse(content)
|
|
||||||
{
|
|
||||||
return Some(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
let value: serde_json::Value = serde_json::from_str(content).ok()?;
|
|
||||||
value
|
|
||||||
.get("context")
|
|
||||||
.and_then(|context| context.as_str())
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn truncate_task_for_signal(task: &str) -> String {
|
|
||||||
const LIMIT: usize = 28;
|
|
||||||
let trimmed = task.trim();
|
|
||||||
let count = trimmed.chars().count();
|
|
||||||
if count <= LIMIT {
|
|
||||||
trimmed.to_string()
|
|
||||||
} else {
|
|
||||||
format!("{}...", trimmed.chars().take(LIMIT - 3).collect::<String>())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn map_conflict_incident(row: &rusqlite::Row<'_>) -> rusqlite::Result<ConflictIncident> {
|
fn map_conflict_incident(row: &rusqlite::Row<'_>) -> rusqlite::Result<ConflictIncident> {
|
||||||
let created_at = parse_timestamp_column(row.get::<_, String>(11)?, 11)?;
|
let created_at = parse_timestamp_column(row.get::<_, String>(11)?, 11)?;
|
||||||
let updated_at = parse_timestamp_column(row.get::<_, String>(12)?, 12)?;
|
let updated_at = parse_timestamp_column(row.get::<_, String>(12)?, 12)?;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use crate::session::output::{
|
|||||||
use crate::session::store::{DaemonActivity, FileActivityOverlap, StateStore};
|
use crate::session::store::{DaemonActivity, FileActivityOverlap, StateStore};
|
||||||
use crate::session::{
|
use crate::session::{
|
||||||
ContextObservationPriority, DecisionLogEntry, FileActivityEntry, Session, SessionGrouping,
|
ContextObservationPriority, DecisionLogEntry, FileActivityEntry, Session, SessionGrouping,
|
||||||
SessionBoardMeta, SessionHarnessInfo, SessionMessage, SessionState,
|
SessionHarnessInfo, SessionMessage, SessionState,
|
||||||
};
|
};
|
||||||
use crate::worktree;
|
use crate::worktree;
|
||||||
|
|
||||||
@@ -93,7 +93,6 @@ pub struct Dashboard {
|
|||||||
approval_queue_counts: HashMap<String, usize>,
|
approval_queue_counts: HashMap<String, usize>,
|
||||||
approval_queue_preview: Vec<SessionMessage>,
|
approval_queue_preview: Vec<SessionMessage>,
|
||||||
handoff_backlog_counts: HashMap<String, usize>,
|
handoff_backlog_counts: HashMap<String, usize>,
|
||||||
board_meta_by_session: HashMap<String, SessionBoardMeta>,
|
|
||||||
worktree_health_by_session: HashMap<String, worktree::WorktreeHealth>,
|
worktree_health_by_session: HashMap<String, worktree::WorktreeHealth>,
|
||||||
global_handoff_backlog_leads: usize,
|
global_handoff_backlog_leads: usize,
|
||||||
global_handoff_backlog_messages: usize,
|
global_handoff_backlog_messages: usize,
|
||||||
@@ -180,7 +179,6 @@ enum Pane {
|
|||||||
Sessions,
|
Sessions,
|
||||||
Output,
|
Output,
|
||||||
Metrics,
|
Metrics,
|
||||||
Board,
|
|
||||||
Log,
|
Log,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,7 +333,7 @@ impl PaneAreas {
|
|||||||
match pane {
|
match pane {
|
||||||
Pane::Sessions => self.sessions = area,
|
Pane::Sessions => self.sessions = area,
|
||||||
Pane::Output => self.output = Some(area),
|
Pane::Output => self.output = Some(area),
|
||||||
Pane::Metrics | Pane::Board => self.metrics = Some(area),
|
Pane::Metrics => self.metrics = Some(area),
|
||||||
Pane::Log => self.log = Some(area),
|
Pane::Log => self.log = Some(area),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -555,7 +553,6 @@ impl Dashboard {
|
|||||||
approval_queue_counts: HashMap::new(),
|
approval_queue_counts: HashMap::new(),
|
||||||
approval_queue_preview: Vec::new(),
|
approval_queue_preview: Vec::new(),
|
||||||
handoff_backlog_counts: HashMap::new(),
|
handoff_backlog_counts: HashMap::new(),
|
||||||
board_meta_by_session: HashMap::new(),
|
|
||||||
worktree_health_by_session: HashMap::new(),
|
worktree_health_by_session: HashMap::new(),
|
||||||
global_handoff_backlog_leads: 0,
|
global_handoff_backlog_leads: 0,
|
||||||
global_handoff_backlog_messages: 0,
|
global_handoff_backlog_messages: 0,
|
||||||
@@ -622,7 +619,6 @@ impl Dashboard {
|
|||||||
dashboard.unread_message_counts = dashboard.db.unread_message_counts().unwrap_or_default();
|
dashboard.unread_message_counts = dashboard.db.unread_message_counts().unwrap_or_default();
|
||||||
dashboard.sync_approval_queue();
|
dashboard.sync_approval_queue();
|
||||||
dashboard.sync_handoff_backlog_counts();
|
dashboard.sync_handoff_backlog_counts();
|
||||||
dashboard.sync_board_meta();
|
|
||||||
dashboard.sync_global_handoff_backlog();
|
dashboard.sync_global_handoff_backlog();
|
||||||
dashboard.sync_selected_output();
|
dashboard.sync_selected_output();
|
||||||
dashboard.sync_selected_diff();
|
dashboard.sync_selected_diff();
|
||||||
@@ -1298,18 +1294,10 @@ impl Dashboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_metrics(&mut self, frame: &mut Frame, area: Rect) {
|
fn render_metrics(&mut self, frame: &mut Frame, area: Rect) {
|
||||||
let side_pane = if self.selected_pane == Pane::Board {
|
|
||||||
Pane::Board
|
|
||||||
} else {
|
|
||||||
Pane::Metrics
|
|
||||||
};
|
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.title(match side_pane {
|
.title(" Metrics ")
|
||||||
Pane::Board => " Board ",
|
.border_style(self.pane_border_style(Pane::Metrics));
|
||||||
_ => " Metrics ",
|
|
||||||
})
|
|
||||||
.border_style(self.pane_border_style(side_pane));
|
|
||||||
let inner = block.inner(area);
|
let inner = block.inner(area);
|
||||||
frame.render_widget(block, area);
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
@@ -1317,17 +1305,6 @@ impl Dashboard {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if side_pane == Pane::Board {
|
|
||||||
frame.render_widget(
|
|
||||||
Paragraph::new(self.board_text())
|
|
||||||
.scroll((self.metrics_scroll_offset as u16, 0))
|
|
||||||
.wrap(Wrap { trim: true }),
|
|
||||||
inner,
|
|
||||||
);
|
|
||||||
self.sync_metrics_scroll(inner.height as usize);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([
|
||||||
@@ -1643,7 +1620,7 @@ impl Dashboard {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if !self.is_pane_visible(target) {
|
if !self.visible_panes().contains(&target) {
|
||||||
self.set_operator_note(format!(
|
self.set_operator_note(format!(
|
||||||
"{} pane is not visible",
|
"{} pane is not visible",
|
||||||
target.title().to_lowercase()
|
target.title().to_lowercase()
|
||||||
@@ -1725,7 +1702,6 @@ impl Dashboard {
|
|||||||
crossterm::event::KeyCode::Char('2') => self.focus_pane_number(2),
|
crossterm::event::KeyCode::Char('2') => self.focus_pane_number(2),
|
||||||
crossterm::event::KeyCode::Char('3') => self.focus_pane_number(3),
|
crossterm::event::KeyCode::Char('3') => self.focus_pane_number(3),
|
||||||
crossterm::event::KeyCode::Char('4') => self.focus_pane_number(4),
|
crossterm::event::KeyCode::Char('4') => self.focus_pane_number(4),
|
||||||
crossterm::event::KeyCode::Char('5') => self.focus_pane_number(5),
|
|
||||||
crossterm::event::KeyCode::Char('+') | crossterm::event::KeyCode::Char('=') => {
|
crossterm::event::KeyCode::Char('+') | crossterm::event::KeyCode::Char('=') => {
|
||||||
self.increase_pane_size()
|
self.increase_pane_size()
|
||||||
}
|
}
|
||||||
@@ -2041,7 +2017,7 @@ impl Dashboard {
|
|||||||
self.output_scroll_offset = self.output_scroll_offset.saturating_add(1);
|
self.output_scroll_offset = self.output_scroll_offset.saturating_add(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Pane::Metrics | Pane::Board => {
|
Pane::Metrics => {
|
||||||
let max_scroll = self.max_metrics_scroll();
|
let max_scroll = self.max_metrics_scroll();
|
||||||
self.metrics_scroll_offset =
|
self.metrics_scroll_offset =
|
||||||
self.metrics_scroll_offset.saturating_add(1).min(max_scroll);
|
self.metrics_scroll_offset.saturating_add(1).min(max_scroll);
|
||||||
@@ -2081,7 +2057,7 @@ impl Dashboard {
|
|||||||
|
|
||||||
self.output_scroll_offset = self.output_scroll_offset.saturating_sub(1);
|
self.output_scroll_offset = self.output_scroll_offset.saturating_sub(1);
|
||||||
}
|
}
|
||||||
Pane::Metrics | Pane::Board => {
|
Pane::Metrics => {
|
||||||
self.metrics_scroll_offset = self.metrics_scroll_offset.saturating_sub(1);
|
self.metrics_scroll_offset = self.metrics_scroll_offset.saturating_sub(1);
|
||||||
}
|
}
|
||||||
Pane::Log => {
|
Pane::Log => {
|
||||||
@@ -4097,7 +4073,6 @@ impl Dashboard {
|
|||||||
};
|
};
|
||||||
self.sync_approval_queue();
|
self.sync_approval_queue();
|
||||||
self.sync_handoff_backlog_counts();
|
self.sync_handoff_backlog_counts();
|
||||||
self.sync_board_meta();
|
|
||||||
self.sync_worktree_health_by_session();
|
self.sync_worktree_health_by_session();
|
||||||
self.sync_session_state_notifications();
|
self.sync_session_state_notifications();
|
||||||
self.sync_approval_notifications();
|
self.sync_approval_notifications();
|
||||||
@@ -4503,7 +4478,7 @@ impl Dashboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_selected_pane_visible(&mut self) {
|
fn ensure_selected_pane_visible(&mut self) {
|
||||||
if !self.is_pane_visible(self.selected_pane) {
|
if !self.visible_panes().contains(&self.selected_pane) {
|
||||||
self.selected_pane = Pane::Sessions;
|
self.selected_pane = Pane::Sessions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4606,16 +4581,6 @@ impl Dashboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_board_meta(&mut self) {
|
|
||||||
self.board_meta_by_session = match self.db.list_session_board_meta() {
|
|
||||||
Ok(meta) => meta,
|
|
||||||
Err(error) => {
|
|
||||||
tracing::warn!("Failed to refresh board metadata: {error}");
|
|
||||||
HashMap::new()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sync_worktree_health_by_session(&mut self) {
|
fn sync_worktree_health_by_session(&mut self) {
|
||||||
self.worktree_health_by_session.clear();
|
self.worktree_health_by_session.clear();
|
||||||
for session in &self.sessions {
|
for session in &self.sessions {
|
||||||
@@ -6532,268 +6497,6 @@ impl Dashboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn board_text(&self) -> String {
|
|
||||||
if self.sessions.is_empty() {
|
|
||||||
return "No sessions available.\n\nStart a session to populate the board.".to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut lines = Vec::new();
|
|
||||||
lines.push(format!("Board snapshot | {} sessions", self.sessions.len()));
|
|
||||||
|
|
||||||
if let Some(session) = self.sessions.get(self.selected_session) {
|
|
||||||
let meta = self.board_meta_by_session.get(&session.id);
|
|
||||||
let branch = session_branch(session);
|
|
||||||
lines.push(format!(
|
|
||||||
"Focus {} {} | {} | {}{}",
|
|
||||||
board_presence_marker(session),
|
|
||||||
board_codename(session),
|
|
||||||
meta.map(|meta| meta.lane.as_str())
|
|
||||||
.unwrap_or_else(|| board_lane_label(&session.state)),
|
|
||||||
format_session_id(&session.id),
|
|
||||||
if branch == "-" {
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
format!(" | {branch}")
|
|
||||||
}
|
|
||||||
));
|
|
||||||
lines.push(format!("Task {}", truncate_for_dashboard(&session.task, 48)));
|
|
||||||
if let Some(meta) = meta {
|
|
||||||
lines.push(format!(
|
|
||||||
"Progress {:>3}% {}",
|
|
||||||
meta.progress_percent,
|
|
||||||
board_progress_bar(meta.progress_percent)
|
|
||||||
));
|
|
||||||
if let Some(status_detail) = meta.status_detail.as_ref() {
|
|
||||||
lines.push(format!("Status {status_detail}"));
|
|
||||||
}
|
|
||||||
if let Some(movement_note) = meta.movement_note.as_ref() {
|
|
||||||
lines.push(format!("Event {movement_note}"));
|
|
||||||
}
|
|
||||||
if meta.handoff_backlog > 0 {
|
|
||||||
lines.push(format!("Inbox {} handoff(s)", meta.handoff_backlog));
|
|
||||||
}
|
|
||||||
if let Some(activity_note) = meta.activity_note.as_ref() {
|
|
||||||
lines.push(format!("Route {activity_note}"));
|
|
||||||
}
|
|
||||||
lines.push(format!(
|
|
||||||
"Coords C{} R{} S{}",
|
|
||||||
meta.column_index + 1,
|
|
||||||
meta.row_index + 1,
|
|
||||||
meta.stack_index + 1
|
|
||||||
));
|
|
||||||
if let Some(row_label) = meta.row_label.as_ref() {
|
|
||||||
lines.push(format!("Row {row_label}"));
|
|
||||||
}
|
|
||||||
if let Some(project) = meta.project.as_ref() {
|
|
||||||
lines.push(format!("Project {project}"));
|
|
||||||
}
|
|
||||||
if let Some(feature) = meta.feature.as_ref() {
|
|
||||||
lines.push(format!("Feature {feature}"));
|
|
||||||
}
|
|
||||||
if let Some(issue) = meta.issue.as_ref() {
|
|
||||||
lines.push(format!("Issue {issue}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let overlap_risks = self.board_overlap_risks();
|
|
||||||
if overlap_risks.is_empty() {
|
|
||||||
lines.push("Overlap risk clear".to_string());
|
|
||||||
} else {
|
|
||||||
lines.push("Overlap risk".to_string());
|
|
||||||
for risk in overlap_risks {
|
|
||||||
lines.push(format!("- {risk}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let lanes = ["Inbox", "In Progress", "Review", "Blocked", "Done", "Stopped"];
|
|
||||||
for label in lanes {
|
|
||||||
let mut lane_sessions = self
|
|
||||||
.sessions
|
|
||||||
.iter()
|
|
||||||
.filter_map(|session| {
|
|
||||||
let lane = self
|
|
||||||
.board_meta_by_session
|
|
||||||
.get(&session.id)
|
|
||||||
.map(|meta| meta.lane.as_str())
|
|
||||||
.unwrap_or_else(|| board_lane_label(&session.state));
|
|
||||||
if lane == label {
|
|
||||||
Some((session, self.board_meta_by_session.get(&session.id)))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
if lane_sessions.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut row_risks: HashMap<(i64, String), Vec<String>> = HashMap::new();
|
|
||||||
let mut row_backlogs: HashMap<(i64, String), i64> = HashMap::new();
|
|
||||||
for (_, meta) in &lane_sessions {
|
|
||||||
let Some(meta) = meta else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let key = (
|
|
||||||
meta.row_index,
|
|
||||||
meta.row_label
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| "General".to_string()),
|
|
||||||
);
|
|
||||||
if let Some(conflict_signal) = meta.conflict_signal.as_ref() {
|
|
||||||
let entry = row_risks.entry(key.clone()).or_default();
|
|
||||||
for risk in conflict_signal.split("; ") {
|
|
||||||
if !entry.iter().any(|existing| existing == risk) {
|
|
||||||
entry.push(risk.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if meta.handoff_backlog > 0 {
|
|
||||||
*row_backlogs.entry(key).or_default() += meta.handoff_backlog;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lane_sessions.sort_by(|left, right| {
|
|
||||||
let left_meta = left.1.cloned().unwrap_or_default();
|
|
||||||
let right_meta = right.1.cloned().unwrap_or_default();
|
|
||||||
left_meta
|
|
||||||
.row_index
|
|
||||||
.cmp(&right_meta.row_index)
|
|
||||||
.then_with(|| left_meta.stack_index.cmp(&right_meta.stack_index))
|
|
||||||
.then_with(|| left.0.id.cmp(&right.0.id))
|
|
||||||
});
|
|
||||||
|
|
||||||
lines.push(String::new());
|
|
||||||
lines.push(format!("{label} ({})", lane_sessions.len()));
|
|
||||||
let mut current_row: Option<String> = None;
|
|
||||||
for (session, meta) in lane_sessions.into_iter().take(6) {
|
|
||||||
let meta = meta.cloned().unwrap_or_default();
|
|
||||||
let row_label = meta
|
|
||||||
.row_label
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| "General".to_string());
|
|
||||||
if current_row.as_ref() != Some(&row_label) {
|
|
||||||
current_row = Some(row_label.clone());
|
|
||||||
let row_key = (meta.row_index, row_label.clone());
|
|
||||||
let row_conflict_summary = row_risks
|
|
||||||
.get(&row_key)
|
|
||||||
.filter(|risks| !risks.is_empty())
|
|
||||||
.map(|risks| truncate_for_dashboard(&risks.join(" + "), 42));
|
|
||||||
let row_backlog = row_backlogs.get(&row_key).copied().unwrap_or(0);
|
|
||||||
let row_pressure_summary = if row_backlog > 0 {
|
|
||||||
Some(format!("{} handoff(s)", row_backlog))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let row_marker = if row_conflict_summary.is_some() {
|
|
||||||
"!"
|
|
||||||
} else if row_pressure_summary.is_some() {
|
|
||||||
"+"
|
|
||||||
} else {
|
|
||||||
"-"
|
|
||||||
};
|
|
||||||
lines.push(format!(
|
|
||||||
" {} Row {} | {}{}{}",
|
|
||||||
row_marker,
|
|
||||||
meta.row_index + 1,
|
|
||||||
row_label,
|
|
||||||
row_conflict_summary
|
|
||||||
.map(|summary| format!(" | {summary}"))
|
|
||||||
.unwrap_or_default(),
|
|
||||||
row_pressure_summary
|
|
||||||
.map(|summary| format!(" | {summary}"))
|
|
||||||
.unwrap_or_default()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let branch = session_branch(session);
|
|
||||||
let branch_suffix = if branch == "-" {
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
format!(" | {branch}")
|
|
||||||
};
|
|
||||||
let activity_suffix = meta
|
|
||||||
.activity_note
|
|
||||||
.as_ref()
|
|
||||||
.map(|note| format!(" | {}", truncate_for_dashboard(note, 26)))
|
|
||||||
.unwrap_or_default();
|
|
||||||
let backlog_suffix = if meta.handoff_backlog > 0 {
|
|
||||||
format!(" | inbox {}", meta.handoff_backlog)
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
let kind_marker = board_activity_marker(&meta);
|
|
||||||
lines.push(format!(
|
|
||||||
" {}{} {} {} {} [{}] {:>3}% {} | {}{}{}{}",
|
|
||||||
board_motion_marker(&meta),
|
|
||||||
kind_marker,
|
|
||||||
board_presence_marker(session),
|
|
||||||
board_codename(session),
|
|
||||||
format_session_id(&session.id),
|
|
||||||
session.agent_type,
|
|
||||||
meta.progress_percent,
|
|
||||||
board_progress_bar(meta.progress_percent),
|
|
||||||
truncate_for_dashboard(meta.status_detail.as_deref().unwrap_or(&session.task), 18),
|
|
||||||
activity_suffix,
|
|
||||||
backlog_suffix,
|
|
||||||
branch_suffix
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn board_overlap_risks(&self) -> Vec<String> {
|
|
||||||
let mut risks = self
|
|
||||||
.board_meta_by_session
|
|
||||||
.values()
|
|
||||||
.filter_map(|meta| meta.conflict_signal.clone())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
if risks.is_empty() {
|
|
||||||
let mut duplicate_branches: HashMap<String, Vec<String>> = HashMap::new();
|
|
||||||
let mut duplicate_tasks: HashMap<String, Vec<String>> = HashMap::new();
|
|
||||||
|
|
||||||
for session in self.sessions.iter().filter(|session| {
|
|
||||||
matches!(
|
|
||||||
session.state,
|
|
||||||
SessionState::Pending
|
|
||||||
| SessionState::Running
|
|
||||||
| SessionState::Idle
|
|
||||||
| SessionState::Stale
|
|
||||||
)
|
|
||||||
}) {
|
|
||||||
if let Some(worktree) = session.worktree.as_ref() {
|
|
||||||
duplicate_branches
|
|
||||||
.entry(worktree.branch.clone())
|
|
||||||
.or_default()
|
|
||||||
.push(format_session_id(&session.id));
|
|
||||||
}
|
|
||||||
duplicate_tasks
|
|
||||||
.entry(session.task.trim().to_ascii_lowercase())
|
|
||||||
.or_default()
|
|
||||||
.push(format_session_id(&session.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (branch, sessions) in duplicate_branches {
|
|
||||||
if sessions.len() >= 2 {
|
|
||||||
risks.push(format!("Shared branch {branch}: {}", sessions.join(", ")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (task, sessions) in duplicate_tasks {
|
|
||||||
if sessions.len() >= 2 {
|
|
||||||
risks.push(format!(
|
|
||||||
"Shared task {}: {}",
|
|
||||||
truncate_for_dashboard(&task, 32),
|
|
||||||
sessions.join(", ")
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
risks.sort();
|
|
||||||
risks.dedup();
|
|
||||||
risks
|
|
||||||
}
|
|
||||||
|
|
||||||
fn aggregate_cost_summary(&self) -> (String, Style) {
|
fn aggregate_cost_summary(&self) -> (String, Style) {
|
||||||
let aggregate = self.aggregate_usage();
|
let aggregate = self.aggregate_usage();
|
||||||
let thresholds = self.cfg.effective_budget_alert_thresholds();
|
let thresholds = self.cfg.effective_budget_alert_thresholds();
|
||||||
@@ -7071,9 +6774,7 @@ impl Dashboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn visible_detail_panes(&self) -> Vec<Pane> {
|
fn visible_detail_panes(&self) -> Vec<Pane> {
|
||||||
self.layout_panes()
|
self.visible_panes()
|
||||||
.into_iter()
|
|
||||||
.filter(|pane| !self.collapsed_panes.contains(pane))
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|pane| *pane != Pane::Sessions)
|
.filter(|pane| *pane != Pane::Sessions)
|
||||||
.collect()
|
.collect()
|
||||||
@@ -7118,19 +6819,6 @@ impl Dashboard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn board_pane_visible(&self) -> bool {
|
|
||||||
self.cfg.pane_layout == PaneLayout::Grid
|
|
||||||
&& !self.collapsed_panes.contains(&Pane::Metrics)
|
|
||||||
&& self.layout_panes().contains(&Pane::Metrics)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_pane_visible(&self, pane: Pane) -> bool {
|
|
||||||
match pane {
|
|
||||||
Pane::Board => self.board_pane_visible(),
|
|
||||||
_ => self.visible_panes().contains(&pane),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn theme_palette(&self) -> ThemePalette {
|
fn theme_palette(&self) -> ThemePalette {
|
||||||
match self.cfg.theme {
|
match self.cfg.theme {
|
||||||
Theme::Dark => ThemePalette {
|
Theme::Dark => ThemePalette {
|
||||||
@@ -7198,7 +6886,6 @@ impl Pane {
|
|||||||
Pane::Sessions => "Sessions",
|
Pane::Sessions => "Sessions",
|
||||||
Pane::Output => "Output",
|
Pane::Output => "Output",
|
||||||
Pane::Metrics => "Metrics",
|
Pane::Metrics => "Metrics",
|
||||||
Pane::Board => "Board",
|
|
||||||
Pane::Log => "Log",
|
Pane::Log => "Log",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7209,7 +6896,6 @@ impl Pane {
|
|||||||
2 => Some(Self::Output),
|
2 => Some(Self::Output),
|
||||||
3 => Some(Self::Metrics),
|
3 => Some(Self::Metrics),
|
||||||
4 => Some(Self::Log),
|
4 => Some(Self::Log),
|
||||||
5 => Some(Self::Board),
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7219,8 +6905,7 @@ impl Pane {
|
|||||||
Self::Sessions => 1,
|
Self::Sessions => 1,
|
||||||
Self::Output => 2,
|
Self::Output => 2,
|
||||||
Self::Metrics => 3,
|
Self::Metrics => 3,
|
||||||
Self::Board => 4,
|
Self::Log => 4,
|
||||||
Self::Log => 5,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7230,7 +6915,6 @@ fn pane_rect(pane_areas: &PaneAreas, pane: Pane) -> Option<Rect> {
|
|||||||
Pane::Sessions => Some(pane_areas.sessions),
|
Pane::Sessions => Some(pane_areas.sessions),
|
||||||
Pane::Output => pane_areas.output,
|
Pane::Output => pane_areas.output,
|
||||||
Pane::Metrics => pane_areas.metrics,
|
Pane::Metrics => pane_areas.metrics,
|
||||||
Pane::Board => pane_areas.metrics,
|
|
||||||
Pane::Log => pane_areas.log,
|
Pane::Log => pane_areas.log,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8563,17 +8247,6 @@ fn diff_addition_word_style() -> Style {
|
|||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn board_lane_label(state: &SessionState) -> &'static str {
|
|
||||||
match state {
|
|
||||||
SessionState::Pending => "Inbox",
|
|
||||||
SessionState::Running => "In Progress",
|
|
||||||
SessionState::Idle => "Review",
|
|
||||||
SessionState::Stale | SessionState::Failed => "Blocked",
|
|
||||||
SessionState::Completed => "Done",
|
|
||||||
SessionState::Stopped => "Stopped",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn session_state_label(state: &SessionState) -> &'static str {
|
fn session_state_label(state: &SessionState) -> &'static str {
|
||||||
match state {
|
match state {
|
||||||
SessionState::Pending => "Pending",
|
SessionState::Pending => "Pending",
|
||||||
@@ -8598,25 +8271,6 @@ fn session_state_color(state: &SessionState) -> Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn board_codename(session: &Session) -> String {
|
|
||||||
const ADJECTIVES: &[&str] = &[
|
|
||||||
"Amber", "Cinder", "Moss", "Nova", "Sable", "Slate", "Swift", "Talon",
|
|
||||||
];
|
|
||||||
const NOUNS: &[&str] = &[
|
|
||||||
"Fox", "Kite", "Lynx", "Otter", "Rook", "Sprite", "Wisp", "Wolf",
|
|
||||||
];
|
|
||||||
|
|
||||||
let seed = session
|
|
||||||
.id
|
|
||||||
.bytes()
|
|
||||||
.fold(0usize, |acc, byte| acc.wrapping_mul(33).wrapping_add(byte as usize));
|
|
||||||
format!(
|
|
||||||
"{} {}",
|
|
||||||
ADJECTIVES[seed % ADJECTIVES.len()],
|
|
||||||
NOUNS[(seed / ADJECTIVES.len()) % NOUNS.len()]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn file_activity_summary(entry: &FileActivityEntry) -> String {
|
fn file_activity_summary(entry: &FileActivityEntry) -> String {
|
||||||
let mut summary = format!(
|
let mut summary = format!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
@@ -9394,44 +9048,6 @@ fn session_branch(session: &Session) -> String {
|
|||||||
.unwrap_or_else(|| "-".to_string())
|
.unwrap_or_else(|| "-".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn board_progress_bar(progress_percent: i64) -> String {
|
|
||||||
let clamped = progress_percent.clamp(0, 100);
|
|
||||||
let filled = ((clamped + 9) / 10) as usize;
|
|
||||||
let empty = 10usize.saturating_sub(filled);
|
|
||||||
format!("[{}{}]", "#".repeat(filled), ".".repeat(empty))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn board_presence_marker(session: &Session) -> String {
|
|
||||||
let codename = board_codename(session);
|
|
||||||
let initials = codename
|
|
||||||
.split_whitespace()
|
|
||||||
.filter_map(|part| part.chars().next())
|
|
||||||
.take(2)
|
|
||||||
.collect::<String>()
|
|
||||||
.to_ascii_uppercase();
|
|
||||||
format!("@{initials}")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn board_motion_marker(meta: &SessionBoardMeta) -> &'static str {
|
|
||||||
match meta.movement_note.as_deref() {
|
|
||||||
Some("Blocked") => "x",
|
|
||||||
Some("Completed") => "*",
|
|
||||||
Some(note) if note.starts_with("Moved ") => ">",
|
|
||||||
Some(note) if note.starts_with("Retargeted ") => "~",
|
|
||||||
_ => ".",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn board_activity_marker(meta: &SessionBoardMeta) -> &'static str {
|
|
||||||
match meta.activity_kind.as_deref() {
|
|
||||||
Some("received") => "<",
|
|
||||||
Some("delegated") => ">",
|
|
||||||
Some("spawned") => "+",
|
|
||||||
Some("spawned_fallback") => "#",
|
|
||||||
_ => "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_duration(duration_secs: u64) -> String {
|
fn format_duration(duration_secs: u64) -> String {
|
||||||
let hours = duration_secs / 3600;
|
let hours = duration_secs / 3600;
|
||||||
let minutes = (duration_secs % 3600) / 60;
|
let minutes = (duration_secs % 3600) / 60;
|
||||||
@@ -14601,11 +14217,6 @@ diff --git a/src/lib.rs b/src/lib.rs
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pane_command_mode_sets_layout() {
|
fn pane_command_mode_sets_layout() {
|
||||||
let tempdir = std::env::temp_dir().join(format!("ecc2-pane-command-{}", Uuid::new_v4()));
|
|
||||||
std::fs::create_dir_all(&tempdir).unwrap();
|
|
||||||
let previous_home = std::env::var_os("HOME");
|
|
||||||
std::env::set_var("HOME", &tempdir);
|
|
||||||
|
|
||||||
let mut dashboard = test_dashboard(Vec::new(), 0);
|
let mut dashboard = test_dashboard(Vec::new(), 0);
|
||||||
dashboard.cfg.pane_layout = PaneLayout::Horizontal;
|
dashboard.cfg.pane_layout = PaneLayout::Horizontal;
|
||||||
|
|
||||||
@@ -14620,22 +14231,10 @@ diff --git a/src/lib.rs b/src/lib.rs
|
|||||||
.operator_note
|
.operator_note
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.is_some_and(|note| note.contains("pane layout set to grid | saved to ")));
|
.is_some_and(|note| note.contains("pane layout set to grid | saved to ")));
|
||||||
|
|
||||||
if let Some(home) = previous_home {
|
|
||||||
std::env::set_var("HOME", home);
|
|
||||||
} else {
|
|
||||||
std::env::remove_var("HOME");
|
|
||||||
}
|
|
||||||
let _ = std::fs::remove_dir_all(tempdir);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cycle_pane_layout_rotates_and_hides_log_when_leaving_grid() {
|
fn cycle_pane_layout_rotates_and_hides_log_when_leaving_grid() {
|
||||||
let tempdir = std::env::temp_dir().join(format!("ecc2-cycle-pane-{}", Uuid::new_v4()));
|
|
||||||
std::fs::create_dir_all(&tempdir).unwrap();
|
|
||||||
let previous_home = std::env::var_os("HOME");
|
|
||||||
std::env::set_var("HOME", &tempdir);
|
|
||||||
|
|
||||||
let mut dashboard = test_dashboard(Vec::new(), 0);
|
let mut dashboard = test_dashboard(Vec::new(), 0);
|
||||||
dashboard.cfg.pane_layout = PaneLayout::Grid;
|
dashboard.cfg.pane_layout = PaneLayout::Grid;
|
||||||
dashboard.cfg.linear_pane_size_percent = 44;
|
dashboard.cfg.linear_pane_size_percent = 44;
|
||||||
@@ -14648,13 +14247,6 @@ diff --git a/src/lib.rs b/src/lib.rs
|
|||||||
assert_eq!(dashboard.cfg.pane_layout, PaneLayout::Horizontal);
|
assert_eq!(dashboard.cfg.pane_layout, PaneLayout::Horizontal);
|
||||||
assert_eq!(dashboard.pane_size_percent, 44);
|
assert_eq!(dashboard.pane_size_percent, 44);
|
||||||
assert_eq!(dashboard.selected_pane, Pane::Sessions);
|
assert_eq!(dashboard.selected_pane, Pane::Sessions);
|
||||||
|
|
||||||
if let Some(home) = previous_home {
|
|
||||||
std::env::set_var("HOME", home);
|
|
||||||
} else {
|
|
||||||
std::env::remove_var("HOME");
|
|
||||||
}
|
|
||||||
let _ = std::fs::remove_dir_all(tempdir);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -14940,7 +14532,6 @@ diff --git a/src/lib.rs b/src/lib.rs
|
|||||||
approval_queue_counts: HashMap::new(),
|
approval_queue_counts: HashMap::new(),
|
||||||
approval_queue_preview: Vec::new(),
|
approval_queue_preview: Vec::new(),
|
||||||
handoff_backlog_counts: HashMap::new(),
|
handoff_backlog_counts: HashMap::new(),
|
||||||
board_meta_by_session: HashMap::new(),
|
|
||||||
worktree_health_by_session: HashMap::new(),
|
worktree_health_by_session: HashMap::new(),
|
||||||
global_handoff_backlog_leads: 0,
|
global_handoff_backlog_leads: 0,
|
||||||
global_handoff_backlog_messages: 0,
|
global_handoff_backlog_messages: 0,
|
||||||
|
|||||||
200
hooks/hooks.json
200
hooks/hooks.json
@@ -7,7 +7,13 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\\\"ecc\\\"],[\\\"ecc@ecc\\\"],[\\\"marketplace\\\",\\\"ecc\\\"],[\\\"everything-claude-code\\\"],[\\\"everything-claude-code@everything-claude-code\\\"],[\\\"marketplace\\\",\\\"everything-claude-code\\\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\\\"ecc\\\",\\\"everything-claude-code\\\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/pre-bash-dispatcher.js"
|
"command": [
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
|
"node",
|
||||||
|
"scripts/hooks/pre-bash-dispatcher.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Consolidated Bash preflight dispatcher for quality, tmux, push, and GateGuard checks",
|
"description": "Consolidated Bash preflight dispatcher for quality, tmux, push, and GateGuard checks",
|
||||||
@@ -18,7 +24,16 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\\\"ecc\\\"],[\\\"ecc@ecc\\\"],[\\\"marketplace\\\",\\\"ecc\\\"],[\\\"everything-claude-code\\\"],[\\\"everything-claude-code@everything-claude-code\\\"],[\\\"marketplace\\\",\\\"everything-claude-code\\\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\\\"ecc\\\",\\\"everything-claude-code\\\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:write:doc-file-warning scripts/hooks/doc-file-warning.js standard,strict"
|
"command": [
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
|
"node",
|
||||||
|
"scripts/hooks/run-with-flags.js",
|
||||||
|
"pre:write:doc-file-warning",
|
||||||
|
"scripts/hooks/doc-file-warning.js",
|
||||||
|
"standard,strict"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Doc file warning: warn about non-standard documentation files (exit code 0; warns only)",
|
"description": "Doc file warning: warn about non-standard documentation files (exit code 0; warns only)",
|
||||||
@@ -29,7 +44,16 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\\\"ecc\\\"],[\\\"ecc@ecc\\\"],[\\\"marketplace\\\",\\\"ecc\\\"],[\\\"everything-claude-code\\\"],[\\\"everything-claude-code@everything-claude-code\\\"],[\\\"marketplace\\\",\\\"everything-claude-code\\\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\\\"ecc\\\",\\\"everything-claude-code\\\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:edit-write:suggest-compact scripts/hooks/suggest-compact.js standard,strict"
|
"command": [
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
|
"node",
|
||||||
|
"scripts/hooks/run-with-flags.js",
|
||||||
|
"pre:edit-write:suggest-compact",
|
||||||
|
"scripts/hooks/suggest-compact.js",
|
||||||
|
"standard,strict"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Suggest manual compaction at logical intervals",
|
"description": "Suggest manual compaction at logical intervals",
|
||||||
@@ -40,7 +64,16 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\\\"ecc\\\"],[\\\"ecc@ecc\\\"],[\\\"marketplace\\\",\\\"ecc\\\"],[\\\"everything-claude-code\\\"],[\\\"everything-claude-code@everything-claude-code\\\"],[\\\"marketplace\\\",\\\"everything-claude-code\\\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\\\"ecc\\\",\\\"everything-claude-code\\\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" shell scripts/hooks/run-with-flags-shell.sh pre:observe skills/continuous-learning-v2/hooks/observe.sh standard,strict",
|
"command": [
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
|
"shell",
|
||||||
|
"scripts/hooks/run-with-flags-shell.sh",
|
||||||
|
"pre:observe",
|
||||||
|
"skills/continuous-learning-v2/hooks/observe.sh",
|
||||||
|
"standard,strict"
|
||||||
|
],
|
||||||
"async": true,
|
"async": true,
|
||||||
"timeout": 10
|
"timeout": 10
|
||||||
}
|
}
|
||||||
@@ -53,7 +86,16 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\\\"ecc\\\"],[\\\"ecc@ecc\\\"],[\\\"marketplace\\\",\\\"ecc\\\"],[\\\"everything-claude-code\\\"],[\\\"everything-claude-code@everything-claude-code\\\"],[\\\"marketplace\\\",\\\"everything-claude-code\\\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\\\"ecc\\\",\\\"everything-claude-code\\\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:governance-capture scripts/hooks/governance-capture.js standard,strict",
|
"command": [
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
|
"node",
|
||||||
|
"scripts/hooks/run-with-flags.js",
|
||||||
|
"pre:governance-capture",
|
||||||
|
"scripts/hooks/governance-capture.js",
|
||||||
|
"standard,strict"
|
||||||
|
],
|
||||||
"timeout": 10
|
"timeout": 10
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -65,7 +107,16 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\\\"ecc\\\"],[\\\"ecc@ecc\\\"],[\\\"marketplace\\\",\\\"ecc\\\"],[\\\"everything-claude-code\\\"],[\\\"everything-claude-code@everything-claude-code\\\"],[\\\"marketplace\\\",\\\"everything-claude-code\\\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\\\"ecc\\\",\\\"everything-claude-code\\\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:config-protection scripts/hooks/config-protection.js standard,strict",
|
"command": [
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
|
"node",
|
||||||
|
"scripts/hooks/run-with-flags.js",
|
||||||
|
"pre:config-protection",
|
||||||
|
"scripts/hooks/config-protection.js",
|
||||||
|
"standard,strict"
|
||||||
|
],
|
||||||
"timeout": 5
|
"timeout": 5
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -77,7 +128,16 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\\\"ecc\\\"],[\\\"ecc@ecc\\\"],[\\\"marketplace\\\",\\\"ecc\\\"],[\\\"everything-claude-code\\\"],[\\\"everything-claude-code@everything-claude-code\\\"],[\\\"marketplace\\\",\\\"everything-claude-code\\\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\\\"ecc\\\",\\\"everything-claude-code\\\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:mcp-health-check scripts/hooks/mcp-health-check.js standard,strict"
|
"command": [
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
|
"node",
|
||||||
|
"scripts/hooks/run-with-flags.js",
|
||||||
|
"pre:mcp-health-check",
|
||||||
|
"scripts/hooks/mcp-health-check.js",
|
||||||
|
"standard,strict"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Check MCP server health before MCP tool execution and block unhealthy MCP calls",
|
"description": "Check MCP server health before MCP tool execution and block unhealthy MCP calls",
|
||||||
@@ -88,7 +148,16 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\\\"ecc\\\"],[\\\"ecc@ecc\\\"],[\\\"marketplace\\\",\\\"ecc\\\"],[\\\"everything-claude-code\\\"],[\\\"everything-claude-code@everything-claude-code\\\"],[\\\"marketplace\\\",\\\"everything-claude-code\\\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\\\"ecc\\\",\\\"everything-claude-code\\\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:edit-write:gateguard-fact-force scripts/hooks/gateguard-fact-force.js standard,strict",
|
"command": [
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
|
"node",
|
||||||
|
"scripts/hooks/run-with-flags.js",
|
||||||
|
"pre:edit-write:gateguard-fact-force",
|
||||||
|
"scripts/hooks/gateguard-fact-force.js",
|
||||||
|
"standard,strict"
|
||||||
|
],
|
||||||
"timeout": 5
|
"timeout": 5
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -102,7 +171,16 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\\\"ecc\\\"],[\\\"ecc@ecc\\\"],[\\\"marketplace\\\",\\\"ecc\\\"],[\\\"everything-claude-code\\\"],[\\\"everything-claude-code@everything-claude-code\\\"],[\\\"marketplace\\\",\\\"everything-claude-code\\\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\\\"ecc\\\",\\\"everything-claude-code\\\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js pre:compact scripts/hooks/pre-compact.js standard,strict"
|
"command": [
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
|
"node",
|
||||||
|
"scripts/hooks/run-with-flags.js",
|
||||||
|
"pre:compact",
|
||||||
|
"scripts/hooks/pre-compact.js",
|
||||||
|
"standard,strict"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Save state before context compaction",
|
"description": "Save state before context compaction",
|
||||||
@@ -115,7 +193,13 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\\\"ecc\\\"],[\\\"ecc@ecc\\\"],[\\\"marketplace\\\",\\\"ecc\\\"],[\\\"everything-claude-code\\\"],[\\\"everything-claude-code@everything-claude-code\\\"],[\\\"marketplace\\\",\\\"everything-claude-code\\\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\\\"ecc\\\",\\\"everything-claude-code\\\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/session-start-bootstrap.js"
|
"command": [
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
|
"node",
|
||||||
|
"scripts/hooks/session-start-bootstrap.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Load previous context and detect package manager on new session",
|
"description": "Load previous context and detect package manager on new session",
|
||||||
@@ -128,7 +212,13 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\\\"ecc\\\"],[\\\"ecc@ecc\\\"],[\\\"marketplace\\\",\\\"ecc\\\"],[\\\"everything-claude-code\\\"],[\\\"everything-claude-code@everything-claude-code\\\"],[\\\"marketplace\\\",\\\"everything-claude-code\\\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\\\"ecc\\\",\\\"everything-claude-code\\\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/post-bash-dispatcher.js",
|
"command": [
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
|
"node",
|
||||||
|
"scripts/hooks/post-bash-dispatcher.js"
|
||||||
|
],
|
||||||
"async": true,
|
"async": true,
|
||||||
"timeout": 30
|
"timeout": 30
|
||||||
}
|
}
|
||||||
@@ -141,7 +231,16 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\\\"ecc\\\"],[\\\"ecc@ecc\\\"],[\\\"marketplace\\\",\\\"ecc\\\"],[\\\"everything-claude-code\\\"],[\\\"everything-claude-code@everything-claude-code\\\"],[\\\"marketplace\\\",\\\"everything-claude-code\\\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\\\"ecc\\\",\\\"everything-claude-code\\\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js post:quality-gate scripts/hooks/quality-gate.js standard,strict",
|
"command": [
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
|
"node",
|
||||||
|
"scripts/hooks/run-with-flags.js",
|
||||||
|
"post:quality-gate",
|
||||||
|
"scripts/hooks/quality-gate.js",
|
||||||
|
"standard,strict"
|
||||||
|
],
|
||||||
"async": true,
|
"async": true,
|
||||||
"timeout": 30
|
"timeout": 30
|
||||||
}
|
}
|
||||||
@@ -154,7 +253,16 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\\\"ecc\\\"],[\\\"ecc@ecc\\\"],[\\\"marketplace\\\",\\\"ecc\\\"],[\\\"everything-claude-code\\\"],[\\\"everything-claude-code@everything-claude-code\\\"],[\\\"marketplace\\\",\\\"everything-claude-code\\\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\\\"ecc\\\",\\\"everything-claude-code\\\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js post:edit:design-quality-check scripts/hooks/design-quality-check.js standard,strict",
|
"command": [
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
|
"node",
|
||||||
|
"scripts/hooks/run-with-flags.js",
|
||||||
|
"post:edit:design-quality-check",
|
||||||
|
"scripts/hooks/design-quality-check.js",
|
||||||
|
"standard,strict"
|
||||||
|
],
|
||||||
"timeout": 10
|
"timeout": 10
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -166,7 +274,16 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\\\"ecc\\\"],[\\\"ecc@ecc\\\"],[\\\"marketplace\\\",\\\"ecc\\\"],[\\\"everything-claude-code\\\"],[\\\"everything-claude-code@everything-claude-code\\\"],[\\\"marketplace\\\",\\\"everything-claude-code\\\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\\\"ecc\\\",\\\"everything-claude-code\\\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js post:edit:accumulate scripts/hooks/post-edit-accumulator.js standard,strict"
|
"command": [
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
|
"node",
|
||||||
|
"scripts/hooks/run-with-flags.js",
|
||||||
|
"post:edit:accumulate",
|
||||||
|
"scripts/hooks/post-edit-accumulator.js",
|
||||||
|
"standard,strict"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Record edited JS/TS file paths for batch format+typecheck at Stop time",
|
"description": "Record edited JS/TS file paths for batch format+typecheck at Stop time",
|
||||||
@@ -177,7 +294,16 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\\\"ecc\\\"],[\\\"ecc@ecc\\\"],[\\\"marketplace\\\",\\\"ecc\\\"],[\\\"everything-claude-code\\\"],[\\\"everything-claude-code@everything-claude-code\\\"],[\\\"marketplace\\\",\\\"everything-claude-code\\\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\\\"ecc\\\",\\\"everything-claude-code\\\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js post:edit:console-warn scripts/hooks/post-edit-console-warn.js standard,strict"
|
"command": [
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
|
"node",
|
||||||
|
"scripts/hooks/run-with-flags.js",
|
||||||
|
"post:edit:console-warn",
|
||||||
|
"scripts/hooks/post-edit-console-warn.js",
|
||||||
|
"standard,strict"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Warn about console.log statements after edits",
|
"description": "Warn about console.log statements after edits",
|
||||||
@@ -188,7 +314,16 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\\\"ecc\\\"],[\\\"ecc@ecc\\\"],[\\\"marketplace\\\",\\\"ecc\\\"],[\\\"everything-claude-code\\\"],[\\\"everything-claude-code@everything-claude-code\\\"],[\\\"marketplace\\\",\\\"everything-claude-code\\\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\\\"ecc\\\",\\\"everything-claude-code\\\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js post:governance-capture scripts/hooks/governance-capture.js standard,strict",
|
"command": [
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
|
"node",
|
||||||
|
"scripts/hooks/run-with-flags.js",
|
||||||
|
"post:governance-capture",
|
||||||
|
"scripts/hooks/governance-capture.js",
|
||||||
|
"standard,strict"
|
||||||
|
],
|
||||||
"timeout": 10
|
"timeout": 10
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -200,7 +335,16 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\\\"ecc\\\"],[\\\"ecc@ecc\\\"],[\\\"marketplace\\\",\\\"ecc\\\"],[\\\"everything-claude-code\\\"],[\\\"everything-claude-code@everything-claude-code\\\"],[\\\"marketplace\\\",\\\"everything-claude-code\\\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\\\"ecc\\\",\\\"everything-claude-code\\\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js post:session-activity-tracker scripts/hooks/session-activity-tracker.js standard,strict",
|
"command": [
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
|
"node",
|
||||||
|
"scripts/hooks/run-with-flags.js",
|
||||||
|
"post:session-activity-tracker",
|
||||||
|
"scripts/hooks/session-activity-tracker.js",
|
||||||
|
"standard,strict"
|
||||||
|
],
|
||||||
"timeout": 10
|
"timeout": 10
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -212,7 +356,16 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\\\"ecc\\\"],[\\\"ecc@ecc\\\"],[\\\"marketplace\\\",\\\"ecc\\\"],[\\\"everything-claude-code\\\"],[\\\"everything-claude-code@everything-claude-code\\\"],[\\\"marketplace\\\",\\\"everything-claude-code\\\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\\\"ecc\\\",\\\"everything-claude-code\\\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" shell scripts/hooks/run-with-flags-shell.sh post:observe skills/continuous-learning-v2/hooks/observe.sh standard,strict",
|
"command": [
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
|
"shell",
|
||||||
|
"scripts/hooks/run-with-flags-shell.sh",
|
||||||
|
"post:observe",
|
||||||
|
"skills/continuous-learning-v2/hooks/observe.sh",
|
||||||
|
"standard,strict"
|
||||||
|
],
|
||||||
"async": true,
|
"async": true,
|
||||||
"timeout": 10
|
"timeout": 10
|
||||||
}
|
}
|
||||||
@@ -227,7 +380,16 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node -e \"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\\\"ecc\\\"],[\\\"ecc@ecc\\\"],[\\\"marketplace\\\",\\\"ecc\\\"],[\\\"everything-claude-code\\\"],[\\\"everything-claude-code@everything-claude-code\\\"],[\\\"marketplace\\\",\\\"everything-claude-code\\\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\\\"ecc\\\",\\\"everything-claude-code\\\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)\" node scripts/hooks/run-with-flags.js post:mcp-health-check scripts/hooks/mcp-health-check.js standard,strict"
|
"command": [
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"const p=require('path');const r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [[\"ecc\"],[\"ecc@ecc\"],[\"marketplace\",\"ecc\"],[\"everything-claude-code\"],[\"everything-claude-code@everything-claude-code\"],[\"marketplace\",\"everything-claude-code\"]]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of [\"ecc\",\"everything-claude-code\"]){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();const s=p.join(r,'scripts/hooks/plugin-hook-bootstrap.js');process.env.CLAUDE_PLUGIN_ROOT=r;process.argv.splice(1,0,s);require(s)",
|
||||||
|
"node",
|
||||||
|
"scripts/hooks/run-with-flags.js",
|
||||||
|
"post:mcp-health-check",
|
||||||
|
"scripts/hooks/mcp-health-check.js",
|
||||||
|
"standard,strict"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Track failed MCP tool calls, mark unhealthy servers, and attempt reconnect",
|
"description": "Track failed MCP tool calls, mark unhealthy servers, and attempt reconnect",
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
"name": "ecc-universal",
|
"name": "ecc-universal",
|
||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
"description": "Complete collection of battle-tested Claude Code configs — agents, skills, hooks, rules, and legacy command shims evolved over 10+ months of intensive daily use by an Anthropic hackathon winner",
|
"description": "Complete collection of battle-tested Claude Code configs — agents, skills, hooks, rules, and legacy command shims evolved over 10+ months of intensive daily use by an Anthropic hackathon winner",
|
||||||
"publishConfig": {
|
|
||||||
"access": "public"
|
|
||||||
},
|
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude-code",
|
"claude-code",
|
||||||
"ai",
|
"ai",
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const assert = require('assert');
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const repoRoot = path.resolve(__dirname, '..', '..');
|
|
||||||
|
|
||||||
let passed = 0;
|
|
||||||
let failed = 0;
|
|
||||||
|
|
||||||
function test(name, fn) {
|
|
||||||
try {
|
|
||||||
fn();
|
|
||||||
console.log(` ✓ ${name}`);
|
|
||||||
passed++;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ✗ ${name}`);
|
|
||||||
console.log(` Error: ${error.message}`);
|
|
||||||
failed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const publicInstallDocs = [
|
|
||||||
'README.md',
|
|
||||||
'README.zh-CN.md',
|
|
||||||
'docs/pt-BR/README.md',
|
|
||||||
'docs/ja-JP/skills/configure-ecc/SKILL.md',
|
|
||||||
'docs/zh-CN/skills/configure-ecc/SKILL.md',
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log('\n=== Testing public install identifiers ===\n');
|
|
||||||
|
|
||||||
for (const relativePath of publicInstallDocs) {
|
|
||||||
const content = fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
|
|
||||||
|
|
||||||
test(`${relativePath} does not use the stale ecc@ecc plugin identifier`, () => {
|
|
||||||
assert.ok(!content.includes('ecc@ecc'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test(`${relativePath} documents the canonical marketplace plugin identifier`, () => {
|
|
||||||
assert.ok(content.includes('everything-claude-code@everything-claude-code'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failed > 0) {
|
|
||||||
console.log(`\nFailed: ${failed}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\nPassed: ${passed}`);
|
|
||||||
@@ -74,7 +74,7 @@ function runTests() {
|
|||||||
|
|
||||||
if (test('post dispatcher writes both bash audit and cost logs in one pass', () => {
|
if (test('post dispatcher writes both bash audit and cost logs in one pass', () => {
|
||||||
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-bash-dispatcher-'));
|
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-bash-dispatcher-'));
|
||||||
const payload = { tool_input: { command: 'npm publish --token=$PUBLISH_TOKEN' } };
|
const payload = { tool_input: { command: 'npm publish --token fixture-token' } };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = runScript(postDispatcher, payload, {
|
const result = runScript(postDispatcher, payload, {
|
||||||
@@ -89,8 +89,8 @@ function runTests() {
|
|||||||
|
|
||||||
assert.ok(auditLog.includes('--token=<REDACTED>'));
|
assert.ok(auditLog.includes('--token=<REDACTED>'));
|
||||||
assert.ok(costLog.includes('tool=Bash command=npm publish --token=<REDACTED>'));
|
assert.ok(costLog.includes('tool=Bash command=npm publish --token=<REDACTED>'));
|
||||||
assert.ok(!auditLog.includes('$PUBLISH_TOKEN'));
|
assert.ok(!auditLog.includes('fixture-token'));
|
||||||
assert.ok(!costLog.includes('$PUBLISH_TOKEN'));
|
assert.ok(!costLog.includes('fixture-token'));
|
||||||
} finally {
|
} finally {
|
||||||
fs.rmSync(homeDir, { recursive: true, force: true });
|
fs.rmSync(homeDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1930,27 +1930,6 @@ async function runTests() {
|
|||||||
passed++;
|
passed++;
|
||||||
else failed++;
|
else failed++;
|
||||||
|
|
||||||
if (
|
|
||||||
test('all hook commands use string form for Claude Code schema compatibility', () => {
|
|
||||||
const hooksPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json');
|
|
||||||
const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf8'));
|
|
||||||
|
|
||||||
for (const [eventName, hookArray] of Object.entries(hooks.hooks)) {
|
|
||||||
for (const entry of hookArray) {
|
|
||||||
for (const hook of entry.hooks) {
|
|
||||||
assert.strictEqual(
|
|
||||||
typeof hook.command,
|
|
||||||
'string',
|
|
||||||
`${eventName}/${entry.id || entry.matcher || 'hook'} should use string command form`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
passed++;
|
|
||||||
else failed++;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
test('all hook commands use node or approved shell wrappers', () => {
|
test('all hook commands use node or approved shell wrappers', () => {
|
||||||
const hooksPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json');
|
const hooksPath = path.join(__dirname, '..', '..', 'hooks', 'hooks.json');
|
||||||
@@ -1989,8 +1968,10 @@ async function runTests() {
|
|||||||
const sessionStartHook = hooks.hooks.SessionStart?.[0]?.hooks?.[0];
|
const sessionStartHook = hooks.hooks.SessionStart?.[0]?.hooks?.[0];
|
||||||
|
|
||||||
assert.ok(sessionStartHook, 'Should define a SessionStart hook');
|
assert.ok(sessionStartHook, 'Should define a SessionStart hook');
|
||||||
const commandText = sessionStartHook.command;
|
const commandText = Array.isArray(sessionStartHook.command)
|
||||||
assert.strictEqual(typeof sessionStartHook.command, 'string', 'SessionStart should use string command form for Claude Code compatibility');
|
? sessionStartHook.command.join(' ')
|
||||||
|
: sessionStartHook.command;
|
||||||
|
assert.ok(Array.isArray(sessionStartHook.command), 'SessionStart should use argv form for cross-platform safety');
|
||||||
assert.ok(
|
assert.ok(
|
||||||
commandText.includes('session-start-bootstrap.js'),
|
commandText.includes('session-start-bootstrap.js'),
|
||||||
'SessionStart should delegate to the extracted bootstrap script'
|
'SessionStart should delegate to the extracted bootstrap script'
|
||||||
|
|||||||
@@ -363,25 +363,25 @@ function runTests() {
|
|||||||
|
|
||||||
const installedBashDispatcherEntry = installedHooks.hooks.PreToolUse.find(entry => entry.id === 'pre:bash:dispatcher');
|
const installedBashDispatcherEntry = installedHooks.hooks.PreToolUse.find(entry => entry.id === 'pre:bash:dispatcher');
|
||||||
assert.ok(installedBashDispatcherEntry, 'hooks/hooks.json should include the consolidated Bash dispatcher hook');
|
assert.ok(installedBashDispatcherEntry, 'hooks/hooks.json should include the consolidated Bash dispatcher hook');
|
||||||
assert.strictEqual(typeof installedBashDispatcherEntry.hooks[0].command, 'string', 'hooks/hooks.json should install string-form commands for Claude Code schema compatibility');
|
assert.ok(Array.isArray(installedBashDispatcherEntry.hooks[0].command), 'hooks/hooks.json should install argv-form commands for cross-platform safety');
|
||||||
assert.ok(
|
assert.ok(
|
||||||
installedBashDispatcherEntry.hooks[0].command.startsWith('node -e '),
|
installedBashDispatcherEntry.hooks[0].command[0] === 'node' && installedBashDispatcherEntry.hooks[0].command[1] === '-e',
|
||||||
'hooks/hooks.json should use the inline node bootstrap contract'
|
'hooks/hooks.json should use the inline node bootstrap contract'
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
installedBashDispatcherEntry.hooks[0].command.includes('plugin-hook-bootstrap.js'),
|
installedBashDispatcherEntry.hooks[0].command.some(part => String(part).includes('plugin-hook-bootstrap.js')),
|
||||||
'hooks/hooks.json should route plugin-managed hooks through the shared bootstrap'
|
'hooks/hooks.json should route plugin-managed hooks through the shared bootstrap'
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
installedBashDispatcherEntry.hooks[0].command.includes('CLAUDE_PLUGIN_ROOT'),
|
installedBashDispatcherEntry.hooks[0].command.some(part => String(part).includes('CLAUDE_PLUGIN_ROOT')),
|
||||||
'hooks/hooks.json should still consult CLAUDE_PLUGIN_ROOT for runtime resolution'
|
'hooks/hooks.json should still consult CLAUDE_PLUGIN_ROOT for runtime resolution'
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
installedBashDispatcherEntry.hooks[0].command.includes('pre-bash-dispatcher.js'),
|
installedBashDispatcherEntry.hooks[0].command.some(part => String(part).includes('pre-bash-dispatcher.js')),
|
||||||
'hooks/hooks.json should point the Bash preflight contract at the consolidated dispatcher'
|
'hooks/hooks.json should point the Bash preflight contract at the consolidated dispatcher'
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!installedBashDispatcherEntry.hooks[0].command.includes('${CLAUDE_PLUGIN_ROOT}'),
|
!installedBashDispatcherEntry.hooks[0].command.some(part => String(part).includes('${CLAUDE_PLUGIN_ROOT}')),
|
||||||
'hooks/hooks.json should not retain raw CLAUDE_PLUGIN_ROOT shell placeholders after install'
|
'hooks/hooks.json should not retain raw CLAUDE_PLUGIN_ROOT shell placeholders after install'
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const assert = require('assert');
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const repoRoot = path.resolve(__dirname, '..', '..');
|
|
||||||
|
|
||||||
let passed = 0;
|
|
||||||
let failed = 0;
|
|
||||||
|
|
||||||
function test(name, fn) {
|
|
||||||
try {
|
|
||||||
fn();
|
|
||||||
console.log(` ✓ ${name}`);
|
|
||||||
passed++;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ✗ ${name}`);
|
|
||||||
console.log(` Error: ${error.message}`);
|
|
||||||
failed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function load(relativePath) {
|
|
||||||
return fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n=== Testing release publish workflow ===\n');
|
|
||||||
|
|
||||||
for (const workflow of [
|
|
||||||
'.github/workflows/release.yml',
|
|
||||||
'.github/workflows/reusable-release.yml',
|
|
||||||
]) {
|
|
||||||
const content = load(workflow);
|
|
||||||
|
|
||||||
test(`${workflow} grants id-token for npm provenance`, () => {
|
|
||||||
assert.match(content, /permissions:\s*[\s\S]*id-token:\s*write/m);
|
|
||||||
});
|
|
||||||
|
|
||||||
test(`${workflow} configures the npm registry`, () => {
|
|
||||||
assert.match(content, /registry-url:\s*['"]https:\/\/registry\.npmjs\.org['"]/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test(`${workflow} checks whether the tagged npm version already exists`, () => {
|
|
||||||
assert.match(content, /Check npm publish state/);
|
|
||||||
assert.match(content, /npm view "\$\{PACKAGE_NAME\}@\$\{PACKAGE_VERSION\}" version/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test(`${workflow} publishes new tag versions to npm`, () => {
|
|
||||||
assert.match(content, /npm publish --access public --provenance/);
|
|
||||||
assert.match(content, /NODE_AUTH_TOKEN:\s*\$\{\{\s*secrets\.NPM_TOKEN\s*\}\}/);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failed > 0) {
|
|
||||||
console.log(`\nFailed: ${failed}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\nPassed: ${passed}`);
|
|
||||||
Reference in New Issue
Block a user