Compare commits
8 Commits
8716f88b4f
...
b152cbd602
| Author | SHA1 | Date |
|---|---|---|
|
|
b152cbd602 | |
|
|
d401afd3f3 | |
|
|
b4d6a92e65 | |
|
|
246270bef2 | |
|
|
79a6876a65 | |
|
|
83f374c254 | |
|
|
9924dc6344 | |
|
|
db7b497eeb |
|
|
@ -1,5 +1,6 @@
|
||||||
code: core
|
code: core
|
||||||
name: "BMad Core Module"
|
name: "BMad Core Module"
|
||||||
|
description: "Core configuration and shared resources"
|
||||||
|
|
||||||
header: "BMad Core Configuration"
|
header: "BMad Core Configuration"
|
||||||
subheader: "Configure the core settings for your BMad installation.\nThese settings will be used across all installed bmad skills, workflows, and agents."
|
subheader: "Configure the core settings for your BMad installation.\nThese settings will be used across all installed bmad skills, workflows, and agents."
|
||||||
|
|
|
||||||
|
|
@ -598,7 +598,7 @@ class UI {
|
||||||
const officialCodes = new Set(officialSelected);
|
const officialCodes = new Set(officialSelected);
|
||||||
const externalManager = new ExternalModuleManager();
|
const externalManager = new ExternalModuleManager();
|
||||||
const registryModules = await externalManager.listAvailable();
|
const registryModules = await externalManager.listAvailable();
|
||||||
const officialRegistryCodes = new Set(registryModules.map((m) => m.code));
|
const officialRegistryCodes = new Set(['core', 'bmm', ...registryModules.map((m) => m.code)]);
|
||||||
const installedNonOfficial = [...installedModuleIds].filter((id) => !officialRegistryCodes.has(id));
|
const installedNonOfficial = [...installedModuleIds].filter((id) => !officialRegistryCodes.has(id));
|
||||||
|
|
||||||
// Phase 2: Community modules (category drill-down)
|
// Phase 2: Community modules (category drill-down)
|
||||||
|
|
@ -630,6 +630,11 @@ class UI {
|
||||||
* @returns {Array} Selected official module codes
|
* @returns {Array} Selected official module codes
|
||||||
*/
|
*/
|
||||||
async _selectOfficialModules(installedModuleIds = new Set()) {
|
async _selectOfficialModules(installedModuleIds = new Set()) {
|
||||||
|
// Built-in modules (core, bmm) come from local source, not the registry
|
||||||
|
const { OfficialModules } = require('./modules/official-modules');
|
||||||
|
const builtInModules = (await new OfficialModules().listAvailable()).modules || [];
|
||||||
|
|
||||||
|
// External modules come from the registry (with fallback)
|
||||||
const externalManager = new ExternalModuleManager();
|
const externalManager = new ExternalModuleManager();
|
||||||
const registryModules = await externalManager.listAvailable();
|
const registryModules = await externalManager.listAvailable();
|
||||||
|
|
||||||
|
|
@ -637,20 +642,34 @@ class UI {
|
||||||
const initialValues = [];
|
const initialValues = [];
|
||||||
const lockedValues = ['core'];
|
const lockedValues = ['core'];
|
||||||
|
|
||||||
const buildModuleEntry = async (mod) => {
|
const buildModuleEntry = async (code, name, description, isDefault) => {
|
||||||
const isInstalled = installedModuleIds.has(mod.code);
|
const isInstalled = installedModuleIds.has(code);
|
||||||
const version = await getMarketplaceVersion(mod.code);
|
const version = await getMarketplaceVersion(code);
|
||||||
const label = version ? `${mod.name} (v${version})` : mod.name;
|
const label = version ? `${name} (v${version})` : name;
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
value: mod.code,
|
value: code,
|
||||||
hint: mod.description,
|
hint: description,
|
||||||
selected: isInstalled,
|
selected: isInstalled || isDefault,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add built-in modules first (always available regardless of network)
|
||||||
|
const builtInCodes = new Set();
|
||||||
|
for (const mod of builtInModules) {
|
||||||
|
const code = mod.id;
|
||||||
|
builtInCodes.add(code);
|
||||||
|
const entry = await buildModuleEntry(code, mod.name, mod.description, mod.defaultSelected);
|
||||||
|
allOptions.push({ label: entry.label, value: entry.value, hint: entry.hint });
|
||||||
|
if (entry.selected) {
|
||||||
|
initialValues.push(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add external registry modules (skip built-in duplicates)
|
||||||
for (const mod of registryModules) {
|
for (const mod of registryModules) {
|
||||||
const entry = await buildModuleEntry(mod);
|
if (mod.builtIn || builtInCodes.has(mod.code)) continue;
|
||||||
|
const entry = await buildModuleEntry(mod.code, mod.name, mod.description, mod.defaultSelected);
|
||||||
allOptions.push({ label: entry.label, value: entry.value, hint: entry.hint });
|
allOptions.push({ label: entry.label, value: entry.value, hint: entry.hint });
|
||||||
if (entry.selected) {
|
if (entry.selected) {
|
||||||
initialValues.push(mod.code);
|
initialValues.push(mod.code);
|
||||||
|
|
@ -1122,12 +1141,26 @@ class UI {
|
||||||
* @returns {Array} Default module codes
|
* @returns {Array} Default module codes
|
||||||
*/
|
*/
|
||||||
async getDefaultModules(installedModuleIds = new Set()) {
|
async getDefaultModules(installedModuleIds = new Set()) {
|
||||||
|
// Built-in modules with default_selected come from local source
|
||||||
|
const { OfficialModules } = require('./modules/official-modules');
|
||||||
|
const builtInModules = (await new OfficialModules().listAvailable()).modules || [];
|
||||||
|
|
||||||
|
const defaultModules = [];
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
for (const mod of builtInModules) {
|
||||||
|
if (mod.defaultSelected || installedModuleIds.has(mod.id)) {
|
||||||
|
defaultModules.push(mod.id);
|
||||||
|
seen.add(mod.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add external registry defaults
|
||||||
const externalManager = new ExternalModuleManager();
|
const externalManager = new ExternalModuleManager();
|
||||||
const registryModules = await externalManager.listAvailable();
|
const registryModules = await externalManager.listAvailable();
|
||||||
|
|
||||||
const defaultModules = [];
|
|
||||||
|
|
||||||
for (const mod of registryModules) {
|
for (const mod of registryModules) {
|
||||||
|
if (mod.builtIn || seen.has(mod.code)) continue;
|
||||||
if (mod.defaultSelected || installedModuleIds.has(mod.code)) {
|
if (mod.defaultSelected || installedModuleIds.has(mod.code)) {
|
||||||
defaultModules.push(mod.code);
|
defaultModules.push(mod.code);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,316 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.9"
|
||||||
|
# dependencies = []
|
||||||
|
# ///
|
||||||
|
"""Remove legacy module directories from _bmad/ after config migration.
|
||||||
|
|
||||||
|
After merge-config.py and merge-help-csv.py have migrated config data and
|
||||||
|
deleted individual legacy files, this script removes the now-redundant
|
||||||
|
directory trees. These directories contain skill files that are already
|
||||||
|
installed at .claude/skills/ (or equivalent) — only the config files at
|
||||||
|
_bmad/ root need to persist.
|
||||||
|
|
||||||
|
When --skills-dir is provided, the script verifies that every skill found
|
||||||
|
in the legacy directories exists at the installed location before removing
|
||||||
|
anything. Directories without skills (like _config/) are removed directly.
|
||||||
|
|
||||||
|
Exit codes: 0=success (including nothing to remove), 1=validation error, 2=runtime error
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Remove legacy module directories from _bmad/ after config migration."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--bmad-dir",
|
||||||
|
required=True,
|
||||||
|
help="Path to the _bmad/ directory",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--module-code",
|
||||||
|
required=True,
|
||||||
|
help="Module code being cleaned up (e.g. 'bmb')",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--also-remove",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help="Additional directory names under _bmad/ to remove (repeatable)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--skills-dir",
|
||||||
|
help="Path to .claude/skills/ — enables safety verification that skills "
|
||||||
|
"are installed before removing legacy copies",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--verbose",
|
||||||
|
action="store_true",
|
||||||
|
help="Print detailed progress to stderr",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def find_skill_dirs(base_path: str) -> list:
|
||||||
|
"""Find installable skill directories under base_path.
|
||||||
|
|
||||||
|
Only considers SKILL.md files at recognized installable positions:
|
||||||
|
- Direct children: base_path/{name}/SKILL.md (legacy flat layout)
|
||||||
|
- Skills subfolder: base_path/skills/{name}/SKILL.md (current layout)
|
||||||
|
|
||||||
|
SKILL.md files nested deeper (e.g. in tasks/, assets/, or within a
|
||||||
|
skill's own subdirectories) are not installable skills and are skipped.
|
||||||
|
|
||||||
|
NOTE: These discovery rules are intentionally stricter than the installer's
|
||||||
|
recursive collectSkills() behavior. The installer is permissive — it walks
|
||||||
|
the entire tree to find all SKILL.md files for installation. Cleanup must
|
||||||
|
be conservative: we only match the two canonical installable layouts so we
|
||||||
|
never accidentally validate a SKILL.md buried in tasks/, assets/, or other
|
||||||
|
non-installable subdirectories as proof that a skill is present.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of skill directory names (e.g. ['bmad-agent-builder', 'bmad-builder-setup'])
|
||||||
|
"""
|
||||||
|
skills = []
|
||||||
|
root = Path(base_path)
|
||||||
|
if not root.exists():
|
||||||
|
return skills
|
||||||
|
|
||||||
|
# Direct child: {name}/SKILL.md
|
||||||
|
for skill_md in root.glob("*/SKILL.md"):
|
||||||
|
skills.append(skill_md.parent.name)
|
||||||
|
|
||||||
|
# Skills subfolder: skills/{name}/SKILL.md
|
||||||
|
skills_root = root / "skills"
|
||||||
|
if skills_root.exists():
|
||||||
|
for skill_md in skills_root.glob("*/SKILL.md"):
|
||||||
|
skills.append(skill_md.parent.name)
|
||||||
|
|
||||||
|
return sorted(set(skills))
|
||||||
|
|
||||||
|
|
||||||
|
def verify_skills_installed(
|
||||||
|
bmad_dir: str, dirs_to_check: list, skills_dir: str, verbose: bool = False
|
||||||
|
) -> list:
|
||||||
|
"""Verify that skills in legacy directories exist at the installed location.
|
||||||
|
|
||||||
|
Scans each directory in dirs_to_check for skill folders (containing SKILL.md),
|
||||||
|
then checks that a matching directory exists under skills_dir. Directories
|
||||||
|
that contain no skills (like _config/) are silently skipped.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of verified skill names.
|
||||||
|
|
||||||
|
Raises SystemExit(1) if any skills are missing from skills_dir.
|
||||||
|
"""
|
||||||
|
all_verified = []
|
||||||
|
missing = []
|
||||||
|
|
||||||
|
for dirname in dirs_to_check:
|
||||||
|
legacy_path = Path(bmad_dir) / dirname
|
||||||
|
if not legacy_path.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
skill_names = find_skill_dirs(str(legacy_path))
|
||||||
|
if not skill_names:
|
||||||
|
if verbose:
|
||||||
|
print(
|
||||||
|
f"No skills found in {dirname}/ — skipping verification",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for skill_name in skill_names:
|
||||||
|
installed_path = Path(skills_dir) / skill_name
|
||||||
|
if installed_path.is_dir():
|
||||||
|
all_verified.append(skill_name)
|
||||||
|
if verbose:
|
||||||
|
print(
|
||||||
|
f"Verified: {skill_name} exists at {installed_path}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
missing.append(skill_name)
|
||||||
|
if verbose:
|
||||||
|
print(
|
||||||
|
f"MISSING: {skill_name} not found at {installed_path}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
error_result = {
|
||||||
|
"status": "error",
|
||||||
|
"error": "Skills not found at installed location",
|
||||||
|
"missing_skills": missing,
|
||||||
|
"skills_dir": str(Path(skills_dir).resolve()),
|
||||||
|
}
|
||||||
|
print(json.dumps(error_result, indent=2))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return sorted(set(all_verified))
|
||||||
|
|
||||||
|
|
||||||
|
def count_files(path: Path) -> int:
|
||||||
|
"""Count all files recursively in a directory."""
|
||||||
|
count = 0
|
||||||
|
for item in path.rglob("*"):
|
||||||
|
if item.is_file():
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_directories(
|
||||||
|
bmad_dir: str, dirs_to_remove: list, verbose: bool = False
|
||||||
|
) -> tuple:
|
||||||
|
"""Remove specified directories under bmad_dir.
|
||||||
|
|
||||||
|
Preserves config.yaml files if present (needed by bmad-init at runtime).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(removed, not_found, total_files_removed) tuple
|
||||||
|
"""
|
||||||
|
removed = []
|
||||||
|
not_found = []
|
||||||
|
total_files = 0
|
||||||
|
|
||||||
|
for dirname in dirs_to_remove:
|
||||||
|
target = Path(bmad_dir) / dirname
|
||||||
|
if not target.exists():
|
||||||
|
not_found.append(dirname)
|
||||||
|
if verbose:
|
||||||
|
print(f"Not found (skipping): {target}", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not target.is_dir():
|
||||||
|
if verbose:
|
||||||
|
print(f"Not a directory (skipping): {target}", file=sys.stderr)
|
||||||
|
not_found.append(dirname)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Validate directory name to prevent path traversal
|
||||||
|
if ".." in dirname or "/" in dirname or "\\" in dirname:
|
||||||
|
error_result = {
|
||||||
|
"status": "error",
|
||||||
|
"error": f"Invalid directory name (path traversal rejected): {dirname}",
|
||||||
|
"directories_removed": removed,
|
||||||
|
"directories_failed": dirname,
|
||||||
|
}
|
||||||
|
print(json.dumps(error_result, indent=2))
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
# Preserve config.yaml if present (bmad-init needs per-module configs)
|
||||||
|
config_path = target / "config.yaml"
|
||||||
|
config_backup = None
|
||||||
|
if config_path.exists():
|
||||||
|
config_backup = config_path.read_bytes()
|
||||||
|
if verbose:
|
||||||
|
print(f"Preserving config.yaml in {dirname}/", file=sys.stderr)
|
||||||
|
|
||||||
|
file_count = count_files(target)
|
||||||
|
if config_backup is not None:
|
||||||
|
file_count -= 1 # Don't count the preserved file
|
||||||
|
if verbose:
|
||||||
|
print(
|
||||||
|
f"Removing {target} ({file_count} files)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(target)
|
||||||
|
|
||||||
|
# Restore preserved config.yaml
|
||||||
|
if config_backup is not None:
|
||||||
|
target.mkdir(parents=True, exist_ok=True)
|
||||||
|
config_path.write_bytes(config_backup)
|
||||||
|
if verbose:
|
||||||
|
print(
|
||||||
|
f"Restored config.yaml in {dirname}/",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
except OSError as e:
|
||||||
|
logger.error("Failed during cleanup of %s: %s", target, e)
|
||||||
|
error_result = {
|
||||||
|
"status": "error",
|
||||||
|
"error": f"Failed to remove {target}: {e}",
|
||||||
|
"directories_removed": removed,
|
||||||
|
"directories_failed": dirname,
|
||||||
|
}
|
||||||
|
print(json.dumps(error_result, indent=2))
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
removed.append(dirname)
|
||||||
|
total_files += file_count
|
||||||
|
|
||||||
|
return removed, not_found, total_files
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
bmad_dir = args.bmad_dir
|
||||||
|
module_code = args.module_code
|
||||||
|
|
||||||
|
# Build the list of directories to remove
|
||||||
|
dirs_to_remove = [module_code, "core"] + args.also_remove
|
||||||
|
# Deduplicate while preserving order
|
||||||
|
seen = set()
|
||||||
|
unique_dirs = []
|
||||||
|
for d in dirs_to_remove:
|
||||||
|
if d not in seen:
|
||||||
|
seen.add(d)
|
||||||
|
unique_dirs.append(d)
|
||||||
|
dirs_to_remove = unique_dirs
|
||||||
|
|
||||||
|
if args.verbose:
|
||||||
|
print(f"Directories to remove: {dirs_to_remove}", file=sys.stderr)
|
||||||
|
|
||||||
|
# Safety check: verify skills are installed before removing
|
||||||
|
verified_skills = None
|
||||||
|
if args.skills_dir:
|
||||||
|
if args.verbose:
|
||||||
|
print(
|
||||||
|
f"Verifying skills installed at {args.skills_dir}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
verified_skills = verify_skills_installed(
|
||||||
|
bmad_dir, dirs_to_remove, args.skills_dir, args.verbose
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove directories
|
||||||
|
removed, not_found, total_files = cleanup_directories(
|
||||||
|
bmad_dir, dirs_to_remove, args.verbose
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build result
|
||||||
|
result = {
|
||||||
|
"status": "success",
|
||||||
|
"bmad_dir": str(Path(bmad_dir).resolve()),
|
||||||
|
"directories_removed": removed,
|
||||||
|
"directories_not_found": not_found,
|
||||||
|
"files_removed_count": total_files,
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.skills_dir:
|
||||||
|
result["safety_checks"] = {
|
||||||
|
"skills_verified": True,
|
||||||
|
"skills_dir": str(Path(args.skills_dir).resolve()),
|
||||||
|
"verified_skills": verified_skills,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
result["safety_checks"] = None
|
||||||
|
|
||||||
|
print(json.dumps(result, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -93,7 +93,6 @@
|
||||||
.agent-icon.john { background: linear-gradient(135deg, #60a5fa, #3b82f6); }
|
.agent-icon.john { background: linear-gradient(135deg, #60a5fa, #3b82f6); }
|
||||||
.agent-icon.sally { background: linear-gradient(135deg, #fbbf24, #f59e0b); color: #000; }
|
.agent-icon.sally { background: linear-gradient(135deg, #fbbf24, #f59e0b); color: #000; }
|
||||||
.agent-icon.winston { background: linear-gradient(135deg, #a78bfa, #8b5cf6); }
|
.agent-icon.winston { background: linear-gradient(135deg, #a78bfa, #8b5cf6); }
|
||||||
.agent-icon.bob { background: linear-gradient(135deg, #34d399, #10b981); color: #000; }
|
|
||||||
.agent-icon.amelia { background: linear-gradient(135deg, #fb7185, #ef4444); }
|
.agent-icon.amelia { background: linear-gradient(135deg, #fb7185, #ef4444); }
|
||||||
|
|
||||||
.agent-name { font-size: 0.65rem; }
|
.agent-name { font-size: 0.65rem; }
|
||||||
|
|
@ -261,7 +260,7 @@
|
||||||
<span class="workflow-name">sprint-planning</span>
|
<span class="workflow-name">sprint-planning</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="workflow-meta">
|
<div class="workflow-meta">
|
||||||
<div class="agent"><div class="agent-icon bob">B</div><span class="agent-name">Bob</span></div>
|
<div class="agent"><div class="agent-icon amelia">A</div><span class="agent-name">Amelia</span></div>
|
||||||
<span class="output">sprint-status.yaml →</span>
|
<span class="output">sprint-status.yaml →</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -270,7 +269,7 @@
|
||||||
<span class="workflow-name">create-story</span>
|
<span class="workflow-name">create-story</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="workflow-meta">
|
<div class="workflow-meta">
|
||||||
<div class="agent"><div class="agent-icon bob">B</div><span class="agent-name">Bob</span></div>
|
<div class="agent"><div class="agent-icon amelia">A</div><span class="agent-name">Amelia</span></div>
|
||||||
<span class="output">story-[slug].md →</span>
|
<span class="output">story-[slug].md →</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -308,7 +307,7 @@
|
||||||
<span class="badge adhoc">par Epic</span>
|
<span class="badge adhoc">par Epic</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="workflow-meta">
|
<div class="workflow-meta">
|
||||||
<div class="agent"><div class="agent-icon bob">B</div><span class="agent-name">Bob</span></div>
|
<div class="agent"><div class="agent-icon amelia">A</div><span class="agent-name">Amelia</span></div>
|
||||||
<span class="output">leçons</span>
|
<span class="output">leçons</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,6 @@
|
||||||
.agent-icon.john { background: linear-gradient(135deg, #60a5fa, #3b82f6); }
|
.agent-icon.john { background: linear-gradient(135deg, #60a5fa, #3b82f6); }
|
||||||
.agent-icon.sally { background: linear-gradient(135deg, #fbbf24, #f59e0b); color: #000; }
|
.agent-icon.sally { background: linear-gradient(135deg, #fbbf24, #f59e0b); color: #000; }
|
||||||
.agent-icon.winston { background: linear-gradient(135deg, #a78bfa, #8b5cf6); }
|
.agent-icon.winston { background: linear-gradient(135deg, #a78bfa, #8b5cf6); }
|
||||||
.agent-icon.bob { background: linear-gradient(135deg, #34d399, #10b981); color: #000; }
|
|
||||||
.agent-icon.amelia { background: linear-gradient(135deg, #fb7185, #ef4444); }
|
.agent-icon.amelia { background: linear-gradient(135deg, #fb7185, #ef4444); }
|
||||||
|
|
||||||
.agent-name { font-size: 0.65rem; }
|
.agent-name { font-size: 0.65rem; }
|
||||||
|
|
@ -272,7 +271,7 @@
|
||||||
<span class="workflow-name">sprint-planning</span>
|
<span class="workflow-name">sprint-planning</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="workflow-meta">
|
<div class="workflow-meta">
|
||||||
<div class="agent"><div class="agent-icon bob">B</div><span class="agent-name">Bob</span></div>
|
<div class="agent"><div class="agent-icon amelia">A</div><span class="agent-name">Amelia</span></div>
|
||||||
<span class="output">sprint-status.yaml →</span>
|
<span class="output">sprint-status.yaml →</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -281,7 +280,7 @@
|
||||||
<span class="workflow-name">create-story</span>
|
<span class="workflow-name">create-story</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="workflow-meta">
|
<div class="workflow-meta">
|
||||||
<div class="agent"><div class="agent-icon bob">B</div><span class="agent-name">Bob</span></div>
|
<div class="agent"><div class="agent-icon amelia">A</div><span class="agent-name">Amelia</span></div>
|
||||||
<span class="output">story-[slug].md →</span>
|
<span class="output">story-[slug].md →</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -319,7 +318,7 @@
|
||||||
<span class="badge adhoc">per epic</span>
|
<span class="badge adhoc">per epic</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="workflow-meta">
|
<div class="workflow-meta">
|
||||||
<div class="agent"><div class="agent-icon bob">B</div><span class="agent-name">Bob</span></div>
|
<div class="agent"><div class="agent-icon amelia">A</div><span class="agent-name">Amelia</span></div>
|
||||||
<span class="output">lessons</span>
|
<span class="output">lessons</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue