diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ff0278fb..c8887b4d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -11,7 +11,7 @@ { "name": "ecc", "source": "./", - "description": "Harness-native ECC operator layer - 63 agents, 249 skills, 79 legacy command shims, reusable hooks, rules, selective install profiles, and production-ready workflows for Claude Code, Codex, OpenCode, Cursor, and related agent harnesses", + "description": "Harness-native ECC operator layer - 63 agents, 251 skills, 79 legacy command shims, reusable hooks, rules, selective install profiles, and production-ready workflows for Claude Code, Codex, OpenCode, Cursor, and related agent harnesses", "version": "2.0.0-rc.1", "author": { "name": "Affaan Mustafa", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 4fb284ff..42db836c 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ecc", "version": "2.0.0-rc.1", - "description": "Harness-native ECC plugin for engineering teams - 63 agents, 249 skills, 79 legacy command shims, reusable hooks, rules, MCP conventions, and operator workflows for Claude Code plus adjacent agent harnesses", + "description": "Harness-native ECC plugin for engineering teams - 63 agents, 251 skills, 79 legacy command shims, reusable hooks, rules, MCP conventions, and operator workflows for Claude Code plus adjacent agent harnesses", "author": { "name": "Affaan Mustafa", "url": "https://x.com/affaanmustafa" diff --git a/AGENTS.md b/AGENTS.md index b956fe2d..fdad0ae2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Everything Claude Code (ECC) — Agent Instructions -This is a **production-ready AI coding plugin** providing 63 specialized agents, 249 skills, 79 commands, and automated hook workflows for software development. +This is a **production-ready AI coding plugin** providing 63 specialized agents, 251 skills, 79 commands, and automated hook workflows for software development. **Version:** 2.0.0-rc.1 @@ -150,7 +150,7 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat ``` agents/ — 63 specialized subagents -skills/ — 249 workflow skills and domain knowledge +skills/ — 251 workflow skills and domain knowledge commands/ — 79 slash commands hooks/ — Trigger-based automations rules/ — Always-follow guidelines (common + per-language) diff --git a/README.md b/README.md index 194cd1cb..c88aaf42 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ This repo is the raw code only. The guides explain everything. ### v2.0.0-rc.1 — Surface Refresh, Operator Workflows, and ECC 2.0 Alpha (Apr 2026) - **Dashboard GUI** — New Tkinter-based desktop application (`ecc_dashboard.py` or `npm run dashboard`) with dark/light theme toggle, font customization, and project logo in header and taskbar. -- **Public surface synced to the live repo** — metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface: 63 agents, 249 skills, and 79 legacy command shims. +- **Public surface synced to the live repo** — metadata, catalog counts, plugin manifests, and install-facing docs now match the actual OSS surface: 63 agents, 251 skills, and 79 legacy command shims. - **Operator and outbound workflow expansion** — `brand-voice`, `social-graph-ranker`, `connections-optimizer`, `customer-billing-ops`, `ecc-tools-cost-audit`, `google-workspace-ops`, `project-flow-ops`, and `workspace-surface-audit` round out the operator lane. - **Media and launch tooling** — `manim-video`, `remotion-video-creation`, and upgraded social publishing surfaces make technical explainers and launch content part of the same system. - **Framework and product surface growth** — `nestjs-patterns`, richer Codex/OpenCode install surfaces, and expanded cross-harness packaging keep the repo usable beyond Claude Code alone. @@ -394,7 +394,7 @@ If you stacked methods, clean up in this order: /plugin list ecc@ecc ``` -**That's it!** You now have access to 63 agents, 249 skills, and 79 legacy command shims. +**That's it!** You now have access to 63 agents, 251 skills, and 79 legacy command shims. ### Dashboard GUI @@ -1427,7 +1427,7 @@ The configuration is automatically detected from `.opencode/opencode.json`. |---------|---------------------|----------|--------| | Agents | PASS: 63 agents | PASS: 12 agents | **Claude Code leads** | | Commands | PASS: 79 commands | PASS: 35 commands | **Claude Code leads** | -| Skills | PASS: 249 skills | PASS: 37 skills | **Claude Code leads** | +| Skills | PASS: 251 skills | PASS: 37 skills | **Claude Code leads** | | Hooks | PASS: 8 event types | PASS: 11 events | **OpenCode has more!** | | Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** | | MCP Servers | PASS: 14 servers | PASS: Full | **Full parity** | @@ -1589,7 +1589,7 @@ ECC is the **first plugin to maximize every major AI coding tool**. Here's how e |---------|-----------------------|------------|-----------|----------|----------------| | **Agents** | 63 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 | N/A | | **Commands** | 79 | Shared | Instruction-based | 35 | 6 prompts | -| **Skills** | 249 | Shared | 10 (native format) | 37 | Via instructions | +| **Skills** | 251 | Shared | 10 (native format) | 37 | Via instructions | | **Hook Events** | 8 types | 15 types | None yet | 11 types | None | | **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks | N/A | | **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions | 1 always-on file | diff --git a/README.zh-CN.md b/README.zh-CN.md index 84979354..0cd0ea8a 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -160,7 +160,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/" /plugin list ecc@ecc ``` -**完成!** 你现在可以使用 63 个代理、249 个技能和 79 个命令。 +**完成!** 你现在可以使用 63 个代理、251 个技能和 79 个命令。 ### multi-* 命令需要额外配置 diff --git a/docs/business/team-agent-orchestration-content-pack.md b/docs/business/team-agent-orchestration-content-pack.md new file mode 100644 index 00000000..94e9d18c --- /dev/null +++ b/docs/business/team-agent-orchestration-content-pack.md @@ -0,0 +1,142 @@ +# Team Agent Orchestration Content Pack + +This pack turns the current ECC direction into publishable ideas without exposing private research sources. The core claim: agent tools are moving from solo chat windows into team orchestration systems with boards, control panes, dynamic workflows, eval gates, and shared skills. + +## Positioning + +ECC should be framed as an orchestration and control-plane layer for the multi-agent stack. The point is not "another prompt library." The point is a workflow operating system for teams that use Claude Code, Codex, OpenCode, Hermes-style desktops, terminal panes, browser agents, MCP gateways, and internal agent tools at the same time. + +## Narrative Thesis + +The old generation of agent Kanban failed because agents were not dependable enough to own real cards. They hallucinated context, skipped verification, and produced output that could not merge. The new generation can work because dynamic workflows, stronger code models, eval harnesses, local state, browser control, and MCP standardization make each card observable and gateable. + +## Video Concepts + +### 1. Why Agent Kanban Failed, And Why It Can Work Now + +- Hook: "Agent Kanban used to be theater. Now it can become the operating surface." +- Show: one card moving from backlog to running to review to merged. +- Key beats: + - Cards need owners, branches, evals, and merge gates. + - Dynamic workflows let agents create task-local harnesses. + - Control panes turn hidden chat output into operational state. +- CTA: "Stop asking if agents can code. Ask whether your team can route, verify, and merge agent work." + +### 2. The Control Pane Is The New IDE Primitive + +- Hook: "The next IDE is not a text editor. It is a mission control surface." +- Show: sessions, work items, memory, connectors, actions, and merge readiness. +- Key beats: + - Teams will run multiple harnesses at once. + - The winning product coordinates context, tools, and evidence. + - Desktop apps matter when they make state inspectable, not when they add another chat box. +- CTA: "Build the pane that tells you what agents are doing, what failed, and what can ship." + +### 3. A Harness For Every Task + +- Hook: "The agent should not just write code. It should build the workflow that proves the code works." +- Show: a dynamic workflow creating tests, browser smoke, and handoff artifacts. +- Key beats: + - Static workflows are good defaults. + - Dynamic workflows are task-local harnesses. + - Repeated dynamic workflows become shared skills. +- CTA: "The real asset is the reusable workflow, not the one-off answer." + +### 4. MCP Gateways And The End Of Reconfiguring Every Agent + +- Hook: "If you configure every MCP server ten times, your agent stack is already broken." +- Show: one tool registry feeding multiple harnesses. +- Key beats: + - Tooling must be centrally declared and locally enforceable. + - The control pane should show connector health. + - Agent portability depends on shared tool contracts. +- CTA: "Treat tools as infrastructure, not per-chat settings." + +### 5. Teams Will Run Like AI Labs + +- Hook: "Every company becomes an AI lab when every workflow has an eval." +- Show: a business workflow with a pass/fail evaluator and a work item queue. +- Key beats: + - Eval gates move agent work from demo to operations. + - Shared skills are team best-practice files. + - The control pane is where management sees throughput and risk. +- CTA: "The future is not one agent. It is an evaluated team of agents." + +## Article Angles + +### 1. Agent Kanban Was Early, Not Wrong + +Argument: + +- Kanban for agents failed when cards were just prompts. +- It starts working when cards carry ownership, branch scope, tests, evals, and handoff. +- Dynamic workflows let each card generate its own proof harness. +- A control pane makes the board honest because it shows state from the filesystem, tests, and sessions. + +Suggested sections: + +1. Why early agent Kanban felt fake. +2. What changed: better models, dynamic workflows, MCP, local state, browser automation. +3. The minimum viable card schema. +4. Why merge gates matter more than task assignment. +5. What teams should build now. + +### 2. The Control Pane Era Of AI Development + +Argument: + +- The next developer surface is a control pane that coordinates agents, tools, memory, and gates. +- Chat remains the interaction layer, but the product value lives in orchestration state. +- ECC should be positioned as the shared layer across local harnesses, desktop agents, and team systems. + +Suggested sections: + +1. Chat is not enough for team work. +2. Sessions, memory, tools, and work items need one pane. +3. Dynamic workflows need visibility. +4. Control panes become the product moat. +5. Open source distribution comes from becoming infrastructure. + +### 3. Shared Skills Are The New Team Playbooks + +Argument: + +- The best companies will not rely on every engineer inventing their own agent workflow. +- A shared skill file is the new best-practices document, but executable by agents. +- Dynamic workflows are discovery; skills are institutional memory. + +Suggested sections: + +1. Why team divergence in agent usage is expensive. +2. What belongs in a skill. +3. When to promote a task-local harness. +4. How evals keep shared skills honest. +5. How this becomes a platform layer. + +## Short Posts + +1. Agent Kanban did not fail because the board was wrong. It failed because the cards had no ownership, eval, branch, or merge gate. The new primitive is not "assign prompt to agent." It is "assign verified work item to agent team." + +2. Dynamic workflows change the unit of reuse. The answer is disposable. The harness is valuable. If the same task-local harness works twice, promote it into a shared skill. + +3. The control pane is where agent work becomes management-visible: who owns the card, what changed, what failed, what passed, and what can merge. + +4. The future OSS wedge for agent infrastructure looks like old infra wedges: become the thing teams install first because it standardizes tools, workflows, evidence, and handoff. + +5. Teams will not run one agent. They will run evaluated squads across code, browser, data, review, and content. The product layer is orchestration. + +## Distribution Plan + +1. Publish one short post on agent Kanban. +2. Follow with a 90-second video showing a card moving through a control pane. +3. Publish the article on shared skills as team playbooks. +4. Release a demo clip of ECC control pane plus a dynamic workflow card. +5. Turn comments into the next skill or article. + +## Product Implications For ECC + +- Build skills first; commands are compatibility shims. +- Make the control pane show work items, agent Kanban state, gates, and reusable-skill candidates. +- Treat dynamic workflows as a feeder system for shared skills. +- Treat MCP and connector configuration as infrastructure that should be visible across harnesses. +- Keep private research private; publish synthesized concepts and product evidence. diff --git a/docs/zh-CN/AGENTS.md b/docs/zh-CN/AGENTS.md index 82979f57..f37808d7 100644 --- a/docs/zh-CN/AGENTS.md +++ b/docs/zh-CN/AGENTS.md @@ -1,6 +1,6 @@ # Everything Claude Code (ECC) — 智能体指令 -这是一个**生产就绪的 AI 编码插件**,提供 63 个专业代理、249 项技能、79 条命令以及自动化钩子工作流,用于软件开发。 +这是一个**生产就绪的 AI 编码插件**,提供 63 个专业代理、251 项技能、79 条命令以及自动化钩子工作流,用于软件开发。 **版本:** 2.0.0-rc.1 @@ -147,7 +147,7 @@ ``` agents/ — 63 个专业子代理 -skills/ — 249 个工作流技能和领域知识 +skills/ — 251 个工作流技能和领域知识 commands/ — 79 个斜杠命令 hooks/ — 基于触发的自动化 rules/ — 始终遵循的指导方针(通用 + 每种语言) diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index e2e4110b..94f5ef9e 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -224,7 +224,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/" /plugin list ecc@ecc ``` -**搞定!** 你现在可以使用 63 个智能体、249 项技能和 79 个命令了。 +**搞定!** 你现在可以使用 63 个智能体、251 项技能和 79 个命令了。 *** @@ -1138,7 +1138,7 @@ opencode |---------|---------------|----------|--------| | 智能体 | PASS: 63 个 | PASS: 12 个 | **Claude Code 领先** | | 命令 | PASS: 79 个 | PASS: 35 个 | **Claude Code 领先** | -| 技能 | PASS: 249 项 | PASS: 37 项 | **Claude Code 领先** | +| 技能 | PASS: 251 项 | PASS: 37 项 | **Claude Code 领先** | | 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** | | 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** | | MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** | @@ -1246,7 +1246,7 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以 |---------|-----------------------|------------|-----------|----------| | **智能体** | 63 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 | | **命令** | 79 | 共享 | 基于指令 | 35 | -| **技能** | 249 | 共享 | 10 (原生格式) | 37 | +| **技能** | 251 | 共享 | 10 (原生格式) | 37 | | **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 | | **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 | | **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 | diff --git a/manifests/install-modules.json b/manifests/install-modules.json index 107a7f8d..4200c26c 100644 --- a/manifests/install-modules.json +++ b/manifests/install-modules.json @@ -631,12 +631,14 @@ "skills/continuous-agent-loop", "skills/cost-aware-llm-pipeline", "skills/data-scraper-agent", + "skills/dynamic-workflow-mode", "skills/enterprise-agent-ops", "skills/nanoclaw-repl", "skills/prompt-optimizer", "skills/ralphinho-rfc-pipeline", "skills/regex-vs-llm-structured-text", "skills/search-first", + "skills/team-agent-orchestration", "skills/token-budget-advisor", "skills/team-builder" ], diff --git a/package.json b/package.json index db60ac2a..25c0d8d4 100644 --- a/package.json +++ b/package.json @@ -168,6 +168,7 @@ "skills/dmux-workflows/", "skills/docker-patterns/", "skills/dotnet-patterns/", + "skills/dynamic-workflow-mode/", "skills/e2e-testing/", "skills/ecc-tools-cost-audit/", "skills/email-ops/", @@ -293,6 +294,7 @@ "skills/swift-protocol-di-testing/", "skills/swiftui-patterns/", "skills/tdd-workflow/", + "skills/team-agent-orchestration/", "skills/team-builder/", "skills/terminal-ops/", "skills/token-budget-advisor/", diff --git a/scripts/control-pane.js b/scripts/control-pane.js index 735e8601..790f2a68 100755 --- a/scripts/control-pane.js +++ b/scripts/control-pane.js @@ -34,6 +34,7 @@ async function main(argv = process.argv) { console.log(`ECC Control Pane: ${app.url}`); console.log(`ECC2 database: ${app.config.dbPath}`); + console.log(`ECC state database: ${app.config.stateDbPath}`); console.log(args.allowActions ? 'Actions: enabled for local allowlist' : 'Actions: read-only'); if (args.openBrowser) { diff --git a/scripts/lib/control-pane/server.js b/scripts/lib/control-pane/server.js index b9df1275..2a1840e3 100644 --- a/scripts/lib/control-pane/server.js +++ b/scripts/lib/control-pane/server.js @@ -12,9 +12,10 @@ const { renderControlPaneHtml } = require('./ui'); function usage() { return [ 'Usage:', - ' node scripts/control-pane.js [--host 127.0.0.1] [--port 8765] [--db ] [--config ] [--query ]', + ' node scripts/control-pane.js [--host 127.0.0.1] [--port 8765] [--db ] [--state-db ] [--config ] [--query ]', '', 'Options:', + ' --state-db Read agent work items from an ECC state-store database', ' --read-only Disable action execution endpoints', ' --no-open Do not open a browser after the server starts', ' --help Show this help', @@ -26,6 +27,15 @@ function valueAfter(args, name) { return index >= 0 ? args[index + 1] : null; } +function pathValueAfter(args, name) { + const value = valueAfter(args, name); + if (value === null) return null; + if (!value || value.startsWith('-')) { + throw new Error(`Invalid ${name} value: expected a path`); + } + return value; +} + function parseArgs(argv) { const args = argv.slice(2); const help = args.includes('--help') || args.includes('-h'); @@ -41,6 +51,7 @@ function parseArgs(argv) { host, port, dbPath: valueAfter(args, '--db'), + stateDbPath: pathValueAfter(args, '--state-db'), configPath: valueAfter(args, '--config'), query: valueAfter(args, '--query') || '', openBrowser: !args.includes('--no-open'), @@ -144,6 +155,7 @@ function createControlPaneServer(options = {}) { cwd: options.cwd || repoRoot, configPath: options.configPath, dbPath: options.dbPath, + stateDbPath: options.stateDbPath, env: options.env || process.env, }); const baseQuery = options.query || ''; @@ -172,6 +184,7 @@ function createControlPaneServer(options = {}) { ok: true, repoRoot, dbPath: resolvedConfig.dbPath, + stateDbPath: resolvedConfig.stateDbPath, allowActions, }); return; @@ -181,6 +194,7 @@ function createControlPaneServer(options = {}) { const snapshot = await buildControlPaneSnapshot({ repoRoot, dbPath: resolvedConfig.dbPath, + stateDbPath: resolvedConfig.stateDbPath, config: resolvedConfig, query: requestUrl.searchParams.get('query') || baseQuery, limit: requestUrl.searchParams.get('limit') || 12, diff --git a/scripts/lib/control-pane/state.js b/scripts/lib/control-pane/state.js index fd742265..c8b313b7 100644 --- a/scripts/lib/control-pane/state.js +++ b/scripts/lib/control-pane/state.js @@ -10,6 +10,7 @@ const toml = require('@iarna/toml'); const { buildControlPaneActions } = require('./actions'); const SNAPSHOT_SCHEMA_VERSION = 'ecc.control-pane.snapshot.v1'; +const DEFAULT_STATE_STORE_RELATIVE_PATH = path.join('.claude', 'ecc', 'state.db'); function homeDir(env = process.env) { return env.HOME || env.USERPROFILE || os.homedir() || '.'; @@ -19,6 +20,10 @@ function defaultDbPath(env = process.env) { return path.join(homeDir(env), '.claude', 'ecc2.db'); } +function defaultStateDbPath(env = process.env) { + return path.join(homeDir(env), DEFAULT_STATE_STORE_RELATIVE_PATH); +} + function defaultConfigPaths(cwd = process.cwd(), env = process.env) { const home = homeDir(env); const paths = [ @@ -75,11 +80,22 @@ function normalizeMemoryConnectors(connectors = {}) { } function normalizeConfig(rawConfig = {}, options = {}) { - const { memory_connectors: snakeMemoryConnectors, memoryConnectors, ...rest } = rawConfig; + const { + memory_connectors: snakeMemoryConnectors, + memoryConnectors, + state_db_path: snakeStateDbPath, + stateDbPath: camelStateDbPath, + ...rest + } = rawConfig; const normalized = normalizeObjectKeys(rest); const connectorConfig = memoryConnectors || snakeMemoryConnectors || normalized.memoryConnectors; return { dbPath: options.dbPath || normalized.dbPath || defaultDbPath(options.env), + stateDbPath: options.stateDbPath + || camelStateDbPath + || snakeStateDbPath + || normalized.stateDbPath + || defaultStateDbPath(options.env), memoryConnectors: normalizeMemoryConnectors(connectorConfig), }; } @@ -107,6 +123,7 @@ function resolveControlPaneConfig(options = {}) { ...normalizeConfig(merged, { env, dbPath: options.dbPath || env.ECC2_DB_PATH || null, + stateDbPath: options.stateDbPath || env.ECC_STATE_DB_PATH || null, }), configPaths: configPaths.filter(configPath => fs.existsSync(configPath)), }; @@ -437,26 +454,120 @@ function connectorStatus(config, db) { }); } +function normalizeWorkItemStatus(status) { + const normalized = String(status || 'open').trim().toLowerCase(); + if (['done', 'closed', 'resolved', 'merged', 'cancelled'].includes(normalized)) return 'done'; + if (['blocked', 'needs-review', 'failed', 'stalled'].includes(normalized)) return 'blocked'; + if (['running', 'in-progress', 'active', 'working'].includes(normalized)) return 'running'; + return 'ready'; +} + +function normalizeWorkItem(row) { + const parsedMetadata = parseJson(row.metadata, {}); + const metadata = isPlainObject(parsedMetadata) ? normalizeObjectKeys(parsedMetadata) : {}; + const kanbanState = normalizeWorkItemStatus(row.status); + return { + id: String(row.id || ''), + source: String(row.source || ''), + sourceId: row.source_id ? String(row.source_id) : null, + title: String(row.title || ''), + status: String(row.status || 'open'), + kanbanState, + priority: row.priority ? String(row.priority) : null, + url: row.url ? String(row.url) : null, + owner: row.owner ? String(row.owner) : null, + repoRoot: row.repo_root ? String(row.repo_root) : null, + sessionId: row.session_id ? String(row.session_id) : null, + branch: metadata.branch || metadata.headRefName || null, + mergeGate: metadata.mergeGate || metadata.mergeGateStatus || metadata.mergeStateStatus || null, + blocker: metadata.blocker || null, + acceptance: Array.isArray(metadata.acceptance) ? metadata.acceptance.map(String) : [], + metadata, + createdAt: String(row.created_at || ''), + updatedAt: String(row.updated_at || ''), + }; +} + +function readWorkItems(db) { + if (!tableExists(db, 'work_items')) return []; + return execRows( + db, + `SELECT * + FROM work_items + ORDER BY updated_at DESC, id DESC + LIMIT 100` + ).map(normalizeWorkItem); +} + +function summarizeWorkItems(items) { + const summary = { + totalCount: items.length, + openCount: 0, + blockedCount: 0, + doneCount: 0, + kanban: { + ready: 0, + running: 0, + blocked: 0, + done: 0, + }, + items, + }; + + for (const item of items) { + const kanbanState = normalizeWorkItemStatus(item.kanbanState || item.status); + summary.kanban[kanbanState] += 1; + if (kanbanState === 'done') { + summary.doneCount += 1; + } else { + summary.openCount += 1; + } + if (kanbanState === 'blocked') summary.blockedCount += 1; + } + + return summary; +} + +async function readWorkItemsSnapshot(stateDbPath) { + let db = null; + try { + db = await openSqlDatabase(stateDbPath); + if (!db) return summarizeWorkItems([]); + return summarizeWorkItems(readWorkItems(db)); + } catch { + return summarizeWorkItems([]); + } finally { + if (db) db.close(); + } +} + async function buildControlPaneSnapshot(options = {}) { const repoRoot = path.resolve(options.repoRoot || path.join(__dirname, '..', '..', '..')); const config = options.config ? normalizeConfig(options.config, { env: options.env || process.env, dbPath: options.dbPath || options.config.dbPath || null, + stateDbPath: options.stateDbPath || options.config.stateDbPath || null, }) : resolveControlPaneConfig(options); const dbPath = options.dbPath || config.dbPath; + const stateDbPath = options.stateDbPath || config.stateDbPath; const query = String(options.query || '').trim(); const limit = Math.max(1, Math.min(Number.parseInt(String(options.limit || 12), 10) || 12, 50)); const generatedAt = new Date().toISOString(); + const workItems = await readWorkItemsSnapshot(stateDbPath); const base = { schemaVersion: SNAPSHOT_SCHEMA_VERSION, generatedAt, repoRoot, dbPath, + stateDbPath, database: { exists: Boolean(dbPath && fs.existsSync(dbPath)), }, + stateDatabase: { + exists: Boolean(stateDbPath && fs.existsSync(stateDbPath)), + }, config: { configPaths: config.configPaths || [], memoryConnectorCount: Object.keys(config.memoryConnectors || {}).length, @@ -473,6 +584,7 @@ async function buildControlPaneSnapshot(options = {}) { results: [], }, connectors: connectorStatus(config, null), + workItems, actions: buildControlPaneActions({ repoRoot, query, limit }), }; @@ -513,6 +625,7 @@ module.exports = { SNAPSHOT_SCHEMA_VERSION, buildControlPaneSnapshot, defaultConfigPaths, + defaultStateDbPath, recallKnowledgeEntries, resolveControlPaneConfig, }; diff --git a/scripts/lib/control-pane/ui.js b/scripts/lib/control-pane/ui.js index f8aae61c..fee3eb5b 100644 --- a/scripts/lib/control-pane/ui.js +++ b/scripts/lib/control-pane/ui.js @@ -215,6 +215,7 @@ function renderControlPaneHtml() { .result, .connector, + .work-item, .action { padding: 12px 14px; border-bottom: 1px solid rgba(52, 64, 56, 0.7); @@ -224,10 +225,42 @@ function renderControlPaneHtml() { .result:last-child, .connector:last-child, + .work-item:last-child, .action:last-child { border-bottom: 0; } + .kanban { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; + padding: 12px 14px; + border-bottom: 1px solid rgba(52, 64, 56, 0.7); + } + + .kanban-lane { + min-width: 0; + padding: 9px; + border: 1px solid rgba(52, 64, 56, 0.8); + border-radius: 6px; + background: #141917; + } + + .kanban-lane span { + display: block; + color: var(--muted); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0; + } + + .kanban-lane strong { + display: block; + margin-top: 6px; + font-size: 20px; + line-height: 1; + } + .row { display: flex; justify-content: space-between; @@ -310,6 +343,7 @@ function renderControlPaneHtml() { @media (max-width: 560px) { main { padding: 12px; } .metrics { grid-template-columns: 1fr; } + .kanban { grid-template-columns: repeat(2, minmax(0, 1fr)); } .query { flex-direction: column; } th:nth-child(4), td:nth-child(4) { display: none; } } @@ -338,6 +372,13 @@ function renderControlPaneHtml() {
+
+
+

Work Items

+ +
+
+
@@ -413,7 +454,13 @@ function renderControlPaneHtml() { function statePill(stateName) { const state = String(stateName || 'unknown'); - const klass = state === 'running' ? 'good' : state === 'failed' ? 'bad' : state === 'pending' ? 'warn' : 'blue'; + const klass = ['running', 'done'].includes(state) + ? 'good' + : ['failed', 'blocked'].includes(state) + ? 'bad' + : ['pending', 'ready'].includes(state) + ? 'warn' + : 'blue'; return '' + escapeHtml(state) + ''; } @@ -446,6 +493,37 @@ function renderControlPaneHtml() { ''; } + function renderWorkItems(workItems) { + const summary = workItems || { totalCount: 0, openCount: 0, blockedCount: 0, doneCount: 0, kanban: {}, items: [] }; + const items = Array.isArray(summary.items) ? summary.items : []; + const kanban = summary.kanban || {}; + $('#work-item-count').textContent = summary.openCount + ' open / ' + summary.blockedCount + ' blocked'; + + const lanes = ['ready', 'running', 'blocked', 'done']; + const laneHtml = '
' + lanes.map(lane => + '
' + escapeHtml(lane) + '' + escapeHtml(kanban[lane] || 0) + '
' + ).join('') + '
'; + + if (!items.length) { + $('#work-items').innerHTML = laneHtml + '
No agent work items found.
'; + return; + } + + $('#work-items').innerHTML = laneHtml + items.slice(0, 8).map(item => { + const branch = item.branch || (item.metadata && item.metadata.branch) || ''; + const mergeGate = item.mergeGate || (item.metadata && item.metadata.mergeGate) || ''; + const blocker = item.blocker || (item.metadata && item.metadata.blocker) || ''; + const owner = item.owner || item.source || 'unassigned'; + return '
' + + '
' + escapeHtml(item.title || item.id) + '' + statePill(item.kanbanState || item.status) + '
' + + '
' + escapeHtml(owner) + ' - ' + escapeHtml(item.source || 'manual') + (item.priority ? ' - ' + escapeHtml(item.priority) : '') + '
' + + (branch ? '
branch: ' + escapeHtml(branch) + '
' : '') + + (mergeGate ? '
merge gate: ' + escapeHtml(mergeGate) + '
' : '') + + (blocker ? '
blocker: ' + escapeHtml(blocker) + '
' : '') + + '
'; + }).join(''); + } + function renderKnowledge(knowledge) { $('#knowledge-count').textContent = knowledge.entityCount + ' entities'; if (!knowledge.results.length) { @@ -526,6 +604,7 @@ function renderControlPaneHtml() { $('#action-status').textContent = snapshot.execution.allowActions ? 'local allowlist' : 'read-only'; renderMetrics(snapshot.summary); renderSessions(snapshot.sessions); + renderWorkItems(snapshot.workItems); renderKnowledge(snapshot.knowledge); renderConnectors(snapshot.connectors); renderActions(snapshot.actions.map(action => ({ diff --git a/skills/dynamic-workflow-mode/SKILL.md b/skills/dynamic-workflow-mode/SKILL.md new file mode 100644 index 00000000..a095ff42 --- /dev/null +++ b/skills/dynamic-workflow-mode/SKILL.md @@ -0,0 +1,123 @@ +--- +name: dynamic-workflow-mode +description: "Design task-local harnesses, eval gates, and reusable skill extraction for Claude dynamic workflow mode and other adaptive agent harnesses." +origin: ECC +--- + +# Dynamic Workflow Mode + +Use this skill when a coding agent can generate or adapt a task-local harness instead of only following a static command flow. The goal is to turn dynamic workflow mode into a disciplined system: temporary harnesses for one-off work, shared skill extraction for repeated work, and observable control pane checkpoints for teams. + +## When To Activate + +- The user mentions dynamic workflows, custom harnesses, harness-per-task, adaptive workflows, or Claude Code dynamic workflow mode. +- A task needs a custom loop, evaluator, crawler, fixture generator, watcher, or local dashboard. +- Multiple agents need the same repeatable process but the process is not yet captured as a shared skill. +- A workflow needs durable handoff artifacts, eval evidence, or operator approval before merge. + +## Core Contract + +Dynamic workflow mode should produce a task-local harness only when the harness is cheaper and safer than manually driving the same steps. The harness must have: + +- **Objective**: the outcome it owns and the outcome it explicitly does not own. +- **Inputs**: files, URLs, prompts, data sources, credentials policy, and user-provided constraints. +- **Outputs**: commits, reports, screenshots, status files, or control pane snapshots. +- **Eval**: at least one pass/fail check tied to the task, not only "it ran". +- **Handoff**: a short artifact that tells the next operator what happened, what is blocked, and how to resume. + +## Dynamic Harness Decision Tree + +1. **One-shot task**: keep it inline. Do not invent a harness. +2. **Repeated task with changing inputs**: create a task-local harness and keep it under a temp or project-local working area. +3. **Repeated task across teammates or repos**: extract the pattern into a shared skill. +4. **Task with external state, queueing, or approvals**: add control pane visibility before adding more automation. +5. **Task with safety risk**: add an eval gate and a human merge gate before autonomous execution. + +## Task-Local Harness Template + +Use this structure before writing code: + +```markdown +# Dynamic Workflow Harness + +Objective: +- Ship: +- Do not ship: + +Inputs: +- Repo or workspace: +- External systems: +- Credentials policy: + +Loop: +1. Discover current state. +2. Generate or update the smallest useful artifact. +3. Run eval checks. +4. Record status and handoff. +5. Stop on failed gate, unclear ownership, or unsafe external action. + +Eval: +- Command: +- Expected pass signal: +- Failure owner: + +Handoff: +- Status: +- Evidence: +- Next action: +``` + +## Shared Skill Extraction + +Promote a task-local harness into a shared skill only when at least two of these are true: + +- The same workflow appears in multiple sessions, repos, teams, or launches. +- The workflow needs specific language, tool, or safety sequencing. +- Failures repeat because operators skip a gate or lose context. +- The workflow has a stable input/output contract. +- The workflow benefits from a control pane, status board, or team handoff. + +When extracting, write the skill first in `skills//SKILL.md`. Add command shims only if a legacy slash-entry surface is still required. + +## Control Pane Checkpoints + +Dynamic workflow mode becomes team-usable when it exposes state. Record these checkpoints whenever the task spans more than one session: + +- **Plan**: objective, owner, acceptance criteria, and risky external systems. +- **Queue**: work items, assigned agent role, branch/worktree, and dependency edges. +- **Run**: active harness, current loop step, recent eval result, and token/cost signal if available. +- **Gate**: test results, browser screenshots, security review, and merge readiness. +- **Handoff**: what is done, what failed, what needs a human decision. + +If the repo has ECC2 state enabled, prefer adding or reading checkpoints through the ECC control pane or state-store-backed scripts instead of scattering untracked notes. + +## Eval Gates + +Every dynamic harness needs a task-specific eval. Pick the cheapest reliable gate: + +| Work Type | Eval Gate | +| --- | --- | +| Code feature | Focused test, lint, coverage, and one integration path | +| UI/control pane | Browser smoke with screenshot and overflow/error checks | +| Agent workflow | Fixture transcript or seeded work item with expected routing | +| Research/content | Source-neutral brief, claim checklist, and publish-ready outline | +| Integration | Dry-run command, config validation, and no-secret scan | + +Do not claim a dynamic workflow is reusable until the eval can be rerun by another teammate. + +## Anti-Patterns + +- Generating scripts that hide the real decision logic from the operator. +- Treating dynamic workflow mode as permission to skip tests. +- Creating one-off docs when a shared skill or status artifact is the real product. +- Running multiple agents without ownership, merge gate, or conflict policy. +- Letting raw private research data leak into public docs. + +## Output Standard + +Finish with: + +- The harness or skill path. +- The eval commands and results. +- The control pane or handoff artifact path. +- The next reusable extraction candidate. diff --git a/skills/team-agent-orchestration/SKILL.md b/skills/team-agent-orchestration/SKILL.md new file mode 100644 index 00000000..128872cc --- /dev/null +++ b/skills/team-agent-orchestration/SKILL.md @@ -0,0 +1,110 @@ +--- +name: team-agent-orchestration +description: "Run team-based orchestration for agent squads using work items, ownership, agent Kanban, merge gates, and control pane handoffs." +origin: ECC +--- + +# Team Agent Orchestration + +Use this skill when agents are being managed like a team rather than a single assistant. The purpose is to make team-based orchestration reliable: clear work items, explicit ownership, agent Kanban state, branch isolation, control pane visibility, and merge gates. + +## When To Activate + +- The task spans multiple agents, tools, harnesses, branches, or worktrees. +- The user mentions team orchestration, agent Kanban, squad, conductor, control pane, manager, desktop app, Zellij, tmux, Hermes, Devin, Codex, Claude Code, or multi-agent work. +- A project needs shared workflow state across people and agents. +- Existing agent fan-out is producing output but not mergeable product. + +## Operating Model + +Treat every agent as a teammate with a narrow contract: + +- **Owner**: the person or agent accountable for the work item. +- **Scope**: files, branch, tool surface, and forbidden areas. +- **State**: backlog, ready, running, review, blocked, merged, or archived. +- **Evidence**: tests, screenshots, logs, review notes, or eval reports. +- **Merge gate**: the exact condition that allows integration. + +## Agent Kanban + +Use agent Kanban when work must be visible across sessions. + +| Column | Meaning | Exit Criteria | +| --- | --- | --- | +| Backlog | Candidate work item, not yet shaped | Acceptance criteria written | +| Ready | Shaped and assignable | Owner and branch/worktree assigned | +| Running | Agent is actively working | Handoff artifact and changed files exist | +| Review | Work is complete but not merged | Tests, diff review, and risk check pass | +| Blocked | Needs external input or failed gate | Blocker has owner and next action | +| Merged | Integrated into mainline | PR merged or local main updated | +| Archived | No longer relevant | Reason recorded | + +Each card should fit this schema: + +```json +{ + "id": "agent-card-001", + "title": "Build dynamic workflow skill", + "owner": "codex", + "state": "running", + "branch": "product/dynamic-workflow-team-orchestration", + "worktree": ".", + "acceptance": [ + "Skill exists", + "Tests cover required concepts", + "Content artifact contains video and article angles" + ], + "merge_gate": "lint, focused tests, and catalog check pass", + "handoff": "path/to/handoff.md" +} +``` + +## Team-Based Orchestration Flow + +1. **Shape the board**: convert fuzzy ambition into work items with owners and merge gates. +2. **Pick execution mode**: single-agent, dynamic workflow mode, dmux/tmux, worktree fan-out, or external desktop orchestrator. +3. **Assign boundaries**: one owner per card, clear file scope, and no overlapping writes without an integrator. +4. **Run agents**: each agent writes evidence and handoff notes, not just code. +5. **Review in sequence**: tests first, then diff review, then security/risk checks, then content/product polish. +6. **Merge deliberately**: one integrator resolves conflicts and updates the control pane or status artifact. +7. **Extract reusable skill**: if the card pattern repeats, promote it into `skills/`. + +## Control Pane Requirements + +A useful control pane for team orchestration should show: + +- Active work items and their agent Kanban state. +- Owner, harness, branch, worktree, and last heartbeat. +- Links to handoff artifacts, tests, screenshots, and PRs. +- Blockers grouped by owner and unblock action. +- Merge readiness by gate, not vibes. +- Reusable workflow candidates that should become shared skills. + +Do not add more automation until the operator can answer: who owns this, what changed, what gate failed, and what can safely merge? + +## Dynamic Workflow Compatibility + +When a card needs dynamic workflow mode: + +- Put the task-local harness under the card owner. +- Store inputs and outputs on the card. +- Require an eval before moving from Running to Review. +- Promote the harness to a shared skill only after repeat use. + +## Failure Modes To Watch + +- **Agent soup**: many agents running, no owner or merge gate. +- **Invisible work**: useful output exists only in a chat transcript. +- **Board theater**: a Kanban board exists but cards have no acceptance criteria. +- **Overlapping writes**: parallel agents edit the same files without worktrees. +- **No product artifact**: the process produces docs but no runnable or publishable surface. + +## Output Standard + +Finish each orchestration pass with: + +- Board/card changes. +- Merged or pending branches. +- Tests and eval evidence. +- Blockers with owner and next action. +- New shared skill candidates. diff --git a/tests/ci/dynamic-workflow-team-surface.test.js b/tests/ci/dynamic-workflow-team-surface.test.js new file mode 100644 index 00000000..3d2bbb99 --- /dev/null +++ b/tests/ci/dynamic-workflow-team-surface.test.js @@ -0,0 +1,104 @@ +#!/usr/bin/env node +/** + * Validate the dynamic workflow and team-orchestration public surface. + */ + +'use strict'; + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +const REPO_ROOT = path.join(__dirname, '..', '..'); + +const SURFACES = [ + { + path: 'skills/dynamic-workflow-mode/SKILL.md', + required: [ + 'dynamic workflow mode', + 'task-local harness', + 'shared skill', + 'eval', + 'control pane', + 'handoff' + ], + }, + { + path: 'skills/team-agent-orchestration/SKILL.md', + required: [ + 'team-based orchestration', + 'agent kanban', + 'work item', + 'ownership', + 'merge gate', + 'control pane' + ], + }, + { + path: 'docs/business/team-agent-orchestration-content-pack.md', + required: [ + 'Video Concepts', + 'Article Angles', + 'agent kanban', + 'team orchestration', + 'dynamic workflows', + 'distribution' + ], + forbidden: [ + 'https://x.com/', + 'http://x.com/', + 'twitter.com/' + ], + }, +]; + +function readSurface(relativePath) { + const absolutePath = path.join(REPO_ROOT, relativePath); + assert.ok(fs.existsSync(absolutePath), `${relativePath} is missing`); + return fs.readFileSync(absolutePath, 'utf8'); +} + +function test(name, fn) { + try { + fn(); + console.log(` PASS ${name}`); + return true; + } catch (error) { + console.log(` FAIL ${name}`); + console.log(` Error: ${error.message}`); + return false; + } +} + +function runTests() { + console.log('\n=== Testing dynamic workflow team surface ===\n'); + + let passed = 0; + let failed = 0; + + for (const surface of SURFACES) { + if (test(`${surface.path} exists and carries required concepts`, () => { + const content = readSurface(surface.path); + const normalized = content.toLowerCase(); + + for (const term of surface.required) { + assert.ok( + normalized.includes(term.toLowerCase()), + `${surface.path} is missing required concept: ${term}` + ); + } + + for (const forbidden of surface.forbidden || []) { + assert.ok( + !normalized.includes(forbidden.toLowerCase()), + `${surface.path} must not expose private bookmark source URLs: ${forbidden}` + ); + } + })) passed++; else failed++; + } + + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); + process.exit(failed > 0 ? 1 : 0); +} + +runTests(); diff --git a/tests/lib/control-pane-state.test.js b/tests/lib/control-pane-state.test.js index c535e70d..642b34fa 100644 --- a/tests/lib/control-pane-state.test.js +++ b/tests/lib/control-pane-state.test.js @@ -227,6 +227,96 @@ async function writeSampleEcc2Database(dbPath) { db.close(); } +async function writeSampleWorkItemsDatabase(dbPath) { + const SQL = await initSqlJs(); + const db = new SQL.Database(); + + db.run(` + CREATE TABLE work_items ( + id TEXT PRIMARY KEY, + source TEXT NOT NULL, + source_id TEXT, + title TEXT NOT NULL, + status TEXT NOT NULL, + priority TEXT, + url TEXT, + owner TEXT, + repo_root TEXT, + session_id TEXT, + metadata TEXT NOT NULL CHECK (json_valid(metadata)), + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + `); + + const insertWorkItem = db.prepare(` + INSERT INTO work_items ( + id, source, source_id, title, status, priority, url, owner, + repo_root, session_id, metadata, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + + insertWorkItem.run([ + 'agent-card-001', + 'manual', + 'agent-card-001', + 'Build dynamic workflow skill', + 'running', + 'high', + null, + 'codex', + '/repo/ecc', + 'lead-hermes', + JSON.stringify({ + branch: 'product/dynamic-workflow-team-orchestration', + mergeGate: 'focused tests and catalog check pass', + acceptance: ['skill exists', 'content pack exists'], + }), + '2026-06-04T09:00:00Z', + '2026-06-04T09:05:00Z', + ]); + insertWorkItem.run([ + 'agent-card-002', + 'github-pr', + '2131', + 'Merge ECC control pane', + 'done', + 'normal', + 'https://github.com/affaan-m/ECC/pull/2131', + 'affaan', + '/repo/ecc', + null, + JSON.stringify({ + branch: 'product/ecc2-knowledge-control-pane', + mergeStateStatus: 'CLEAN', + }), + '2026-06-03T13:00:00Z', + '2026-06-03T13:55:00Z', + ]); + insertWorkItem.run([ + 'agent-card-003', + 'manual', + 'blocked-content', + 'Record content pipeline', + 'blocked', + 'high', + null, + 'operator', + '/repo/ecc', + null, + JSON.stringify({ + blocker: 'needs publish approval', + mergeGate: 'approval packet accepted', + }), + '2026-06-04T09:10:00Z', + '2026-06-04T09:12:00Z', + ]); + insertWorkItem.free(); + + fs.writeFileSync(dbPath, Buffer.from(db.export())); + db.close(); +} + async function mutateSqlDatabase(dbPath, mutator) { const SQL = await initSqlJs(); const buffer = fs.readFileSync(dbPath); @@ -290,6 +380,64 @@ async function runTests() { } })) passed++; else failed++; + if (await test('projects state-store work items into agent Kanban summary', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-work-items-')); + const dbPath = path.join(tempDir, 'ecc2.db'); + const stateDbPath = path.join(tempDir, 'state.db'); + + try { + await writeSampleEcc2Database(dbPath); + await writeSampleWorkItemsDatabase(stateDbPath); + + const snapshot = await buildControlPaneSnapshot({ + dbPath, + stateDbPath, + repoRoot: path.join(__dirname, '..', '..'), + query: 'workflow', + }); + + assert.strictEqual(snapshot.workItems.totalCount, 3); + assert.strictEqual(snapshot.workItems.openCount, 2); + assert.strictEqual(snapshot.workItems.blockedCount, 1); + assert.strictEqual(snapshot.workItems.doneCount, 1); + assert.strictEqual(snapshot.workItems.kanban.running, 1); + assert.strictEqual(snapshot.workItems.kanban.blocked, 1); + assert.strictEqual(snapshot.workItems.items[0].id, 'agent-card-003'); + assert.strictEqual(snapshot.workItems.items[0].mergeGate, 'approval packet accepted'); + assert.strictEqual(snapshot.workItems.items[1].branch, 'product/dynamic-workflow-team-orchestration'); + assert.strictEqual( + snapshot.workItems.items.find(item => item.id === 'agent-card-002').mergeGate, + 'CLEAN' + ); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + })) passed++; else failed++; + + if (await test('treats an unreadable optional state-store database as empty work items', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-corrupt-work-items-')); + const dbPath = path.join(tempDir, 'ecc2.db'); + const stateDbPath = path.join(tempDir, 'corrupt-state.db'); + + try { + await writeSampleEcc2Database(dbPath); + fs.writeFileSync(stateDbPath, 'not a sqlite database', 'utf8'); + + const snapshot = await buildControlPaneSnapshot({ + dbPath, + stateDbPath, + repoRoot: path.join(__dirname, '..', '..'), + query: 'workflow', + }); + + assert.strictEqual(snapshot.stateDatabase.exists, true); + assert.strictEqual(snapshot.workItems.totalCount, 0); + assert.strictEqual(snapshot.summary.totalSessions, 2); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + })) passed++; else failed++; + if (await test('resolves config from explicit db path and TOML connector file', async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-config-')); const dbPath = path.join(tempDir, 'state.db'); diff --git a/tests/scripts/control-pane.test.js b/tests/scripts/control-pane.test.js index e7e734f5..6a3fa033 100644 --- a/tests/scripts/control-pane.test.js +++ b/tests/scripts/control-pane.test.js @@ -148,6 +148,8 @@ async function runTests() { '8788', '--db', '/tmp/ecc2.db', + '--state-db', + '/tmp/ecc-state.db', '--query', 'Hermes memory', '--no-open', @@ -156,6 +158,7 @@ async function runTests() { assert.strictEqual(parsed.host, '127.0.0.1'); assert.strictEqual(parsed.port, 8788); assert.strictEqual(parsed.dbPath, '/tmp/ecc2.db'); + assert.strictEqual(parsed.stateDbPath, '/tmp/ecc-state.db'); assert.strictEqual(parsed.query, 'Hermes memory'); assert.strictEqual(parsed.openBrowser, false); })) passed++; else failed++; @@ -171,6 +174,17 @@ async function runTests() { ); })) passed++; else failed++; + if (await test('rejects missing state database path values', async () => { + assert.throws( + () => parseArgs(['node', 'scripts/control-pane.js', '--state-db']), + /Invalid --state-db value/ + ); + assert.throws( + () => parseArgs(['node', 'scripts/control-pane.js', '--state-db', '--query', 'Hermes']), + /Invalid --state-db value/ + ); + })) passed++; else failed++; + if (await test('serves HTML and snapshot JSON from a temp ECC2 database', async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-control-pane-server-')); const dbPath = path.join(tempDir, 'ecc2.db'); @@ -191,12 +205,15 @@ async function runTests() { const html = await fetchLocal(`${app.url}/`).then(response => response.text()); assert.ok(html.includes('ECC Control Pane')); assert.ok(html.includes('id="app"')); + assert.ok(html.includes('id="work-items"')); + assert.ok(html.includes('function renderWorkItems')); assert.ok(html.includes('function showError')); assert.ok(html.includes('response.ok')); const snapshot = await fetchLocal(`${app.url}/api/snapshot?query=control`).then(response => response.json()); assert.strictEqual(snapshot.schemaVersion, 'ecc.control-pane.snapshot.v1'); assert.strictEqual(snapshot.summary.totalSessions, 1); + assert.strictEqual(snapshot.workItems.totalCount, 0); assert.strictEqual(snapshot.sessions[0].id, 'session-a'); } finally { await app.close();