feat(ADM-018): completed feature

This commit is contained in:
chattie
2026-08-17 22:23:10 +02:00
parent cf1c69fc8b
commit d595b4871f
871 changed files with 47411 additions and 281 deletions

View File

@@ -213,6 +213,7 @@ def validate_stage_transition(status, args):
stage = args.stage or status.get('stage') or 'idle'
agent = args.agent or status.get('agent') or STAGE_OWNERS.get(stage)
feature_id = args.feature_id if args.feature_id is not None else status.get('feature_id')
state = args.state
expected_owner = STAGE_OWNERS.get(stage)
if expected_owner and agent != expected_owner:
@@ -227,6 +228,16 @@ def validate_stage_transition(status, args):
suffix = f" con verdict {verdict}" if verdict else ""
raise SystemExit(f"No se puede entrar en {stage}: falta {filename}{suffix} para {feature_id}")
# Block marking close stage as done without all required gates
if stage == 'close' and state == 'done' and feature_id:
missing_gates = []
for gate_agent in ['reviewer', 'security', 'qa']:
gate_file = GATE_FILES[gate_agent]
if not artifact_ok(feature_id, gate_file, 'APPROVED'):
missing_gates.append(f"{gate_file} (APPROVED)")
if missing_gates:
raise SystemExit(f"No se puede cerrar {feature_id}: faltan gates aprobados: {', '.join(missing_gates)}")
def validate_runtime_args(args):
roles = set(load_roles()) or set(DEFAULT_EMOJIS)

119
scripts/close_feature.py Executable file
View File

@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""
Close a feature: validates all gates are approved, updates backlog, and runs commit.
Usage: python3 scripts/close_feature.py <feature_id>
"""
import json
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
BACKLOG_PATH = ROOT / 'backlog' / 'features.json'
ARTIFACTS_DIR = ROOT / 'work' / 'artifacts'
COMMIT_SCRIPT = ROOT / 'scripts' / 'commit_feature.sh'
def now_iso():
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace('+00:00', 'Z')
def check_gate(feature_id, gate_file, expected_verdict='APPROVED'):
"""Check if a gate artifact exists and has the expected verdict."""
path = ARTIFACTS_DIR / feature_id / gate_file
if not path.exists():
return False, f"Missing {gate_file}"
try:
data = json.loads(path.read_text(encoding='utf-8'))
if data.get('verdict') != expected_verdict:
return False, f"{gate_file} verdict is {data.get('verdict')}, expected {expected_verdict}"
return True, None
except Exception as e:
return False, f"{gate_file} invalid: {e}"
def close_feature(feature_id):
"""Close a feature after validating all gates."""
print(f"Closing feature {feature_id}...")
# Check all required gates
gates = [
('reviewer.json', 'APPROVED'),
('security.json', 'APPROVED'),
('qa.json', 'APPROVED'),
('leader-close.json', None), # Just needs to exist
]
errors = []
for gate_file, verdict in gates:
if verdict:
ok, err = check_gate(feature_id, gate_file, verdict)
else:
path = ARTIFACTS_DIR / feature_id / gate_file
ok = path.exists()
err = f"Missing {gate_file}" if not ok else None
if not ok:
errors.append(err)
print(f"{err}")
else:
print(f"{gate_file}")
if errors:
print(f"\n[FAIL] Cannot close {feature_id}: {len(errors)} gate(s) missing or invalid")
return 1
# Update backlog
if not BACKLOG_PATH.exists():
print(f"[FAIL] Backlog not found: {BACKLOG_PATH}")
return 1
try:
backlog = json.loads(BACKLOG_PATH.read_text(encoding='utf-8'))
except Exception as e:
print(f"[FAIL] Invalid backlog: {e}")
return 1
feature_found = False
for feature in backlog.get('features', []):
if feature.get('id') == feature_id:
feature['status'] = 'done'
feature['completed_at'] = now_iso()
feature_found = True
print(f"\n[OK] Updated backlog: {feature_id} -> done")
break
if not feature_found:
print(f"[FAIL] Feature {feature_id} not found in backlog")
return 1
# Save backlog
try:
BACKLOG_PATH.write_text(json.dumps(backlog, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
except Exception as e:
print(f"[FAIL] Cannot save backlog: {e}")
return 1
# Run commit script
if COMMIT_SCRIPT.exists():
print(f"\nRunning commit script...")
result = subprocess.run([str(COMMIT_SCRIPT), feature_id], cwd=ROOT)
if result.returncode != 0:
print(f"[WARN] Commit script returned {result.returncode}")
print(f"\n✅ Feature {feature_id} closed successfully")
return 0
def main():
if len(sys.argv) != 2:
print("Usage: python3 scripts/close_feature.py <feature_id>")
return 1
feature_id = sys.argv[1]
return close_feature(feature_id)
if __name__ == '__main__':
sys.exit(main())

58
scripts/commit_feature.sh Executable file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Commit and optionally push a completed feature
# Usage: ./scripts/commit_feature.sh <feature_id>
set -euo pipefail
FEATURE_ID="${1:-}"
if [ -z "$FEATURE_ID" ]; then
echo "[FAIL] Usage: $0 <feature_id>" >&2
exit 1
fi
# Check if git repo
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "[SKIP] Not a git repository, skipping commit"
exit 0
fi
# Stage all changes (excluding node_modules, dist, .next, etc.)
git add -A
# Check if there are changes to commit
if git diff --cached --quiet; then
echo "[SKIP] No changes to commit for $FEATURE_ID"
exit 0
fi
# Create commit message
COMMIT_MSG="feat($FEATURE_ID): completed feature"
# Check if there's a description in the feature
if [ -f "work/current.md" ]; then
# Extract first line of description if available
DESC=$(grep -m1 "^## Description" work/current.md -A1 2>/dev/null | tail -1 | sed 's/^[[:space:]]*//' || true)
if [ -n "$DESC" ]; then
COMMIT_MSG="$COMMIT_MSG
$DESC"
fi
fi
# Commit
git commit -m "$COMMIT_MSG"
echo "[OK] Committed $FEATURE_ID"
# Check if remote exists and push
if git remote get-url origin >/dev/null 2>&1; then
BRANCH=$(git rev-parse --abbrev-ref HEAD)
echo "[INFO] Pushing to origin/$BRANCH..."
if git push origin "$BRANCH"; then
echo "[OK] Pushed to origin/$BRANCH"
else
echo "[WARN] Push failed (you can push manually later)"
fi
else
echo "[SKIP] No remote 'origin' configured, skipping push"
fi

View File

@@ -0,0 +1,248 @@
#!/usr/bin/env python3
"""
Fix Orquestra violations in new-mercadodevida.
This script performs all the corrective actions identified in the audit.
"""
import json
import shutil
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
BACKLOG_PATH = ROOT / 'backlog' / 'features.json'
ARTIFACTS_DIR = ROOT / 'work' / 'artifacts'
RUNTIME_STATUS = ROOT / 'work' / 'runtime-status.json'
SPECS_DIR = ROOT / 'specs'
LEGACY_DIR = ROOT / 'legacy'
def now_iso():
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace('+00:00', 'Z')
def fix_specs_directory():
"""Archive specs/ (plural) to legacy/"""
if not SPECS_DIR.exists():
print(" ✅ specs/ directory does not exist (already clean)")
return
print(f" 📦 Moving specs/ to legacy/specs-old/")
LEGACY_DIR.mkdir(exist_ok=True)
dest = LEGACY_DIR / 'specs-old'
if dest.exists():
shutil.rmtree(dest)
shutil.move(str(SPECS_DIR), str(dest))
print(f" ✅ Archived specs/ to legacy/specs-old/")
def fix_features_without_valid_gates(backlog):
"""Move features done without valid gates to pending"""
fixed = []
for feature in backlog.get('features', []):
if feature.get('status') != 'done':
continue
feature_id = feature.get('id')
gates = feature.get('gates', {})
# Check if gates are valid (all required gates approved)
required_gates = ['reviewer', 'security', 'qa']
has_valid_gates = all(gates.get(gate) is True for gate in required_gates)
if not has_valid_gates:
# Check if it's an old feature with 'review' instead of 'reviewer'
if gates.get('review') is True and gates.get('security') is True and gates.get('qa') is True:
# This is an old feature with correct gates but wrong nomenclature
continue
# Move to pending
feature['status'] = 'pending'
if 'completed_at' in feature:
del feature['completed_at']
fixed.append(feature_id)
return fixed
def fix_features_without_artifacts(backlog):
"""Move features done without artifacts to pending"""
fixed = []
for feature in backlog.get('features', []):
if feature.get('status') != 'done':
continue
feature_id = feature.get('id')
artifact_dir = ARTIFACTS_DIR / feature_id
if not artifact_dir.exists():
# Feature is done but has no artifacts
feature['status'] = 'pending'
if 'completed_at' in feature:
del feature['completed_at']
fixed.append(feature_id)
return fixed
def normalize_gate_nomenclature(backlog):
"""Change 'review' to 'reviewer' in features"""
normalized = []
for feature in backlog.get('features', []):
gates = feature.get('gates', {})
if 'review' in gates and 'reviewer' not in gates:
gates['reviewer'] = gates.pop('review')
feature['gates'] = gates
normalized.append(feature.get('id'))
return normalized
def add_completed_at_to_done_features(backlog):
"""Add completed_at timestamp to features that are done but missing it"""
fixed = []
for feature in backlog.get('features', []):
if feature.get('status') == 'done' and not feature.get('completed_at'):
# Use a default timestamp (we don't know when it was actually completed)
feature['completed_at'] = '2026-08-17T00:00:00Z'
fixed.append(feature.get('id'))
return fixed
def standardize_feature_schema(backlog):
"""Ensure all features have minimum required fields"""
standardized = []
for feature in backlog.get('features', []):
# Add missing fields with defaults
if 'type' not in feature:
feature['type'] = 'feature'
if 'priority' not in feature:
feature['priority'] = 'medium'
if 'gates' not in feature:
feature['gates'] = {}
standardized.append(feature.get('id'))
return standardized
def sync_runtime_status(backlog):
"""Sync runtime-status.json with features.json"""
if not RUNTIME_STATUS.exists():
print(" ⚠️ runtime-status.json does not exist")
return
try:
status = json.loads(RUNTIME_STATUS.read_text(encoding='utf-8'))
feature_id = status.get('feature_id')
if not feature_id:
print(" ✅ No active feature in runtime-status.json")
return
# Find the feature in backlog
feature_found = None
for feature in backlog.get('features', []):
if feature.get('id') == feature_id:
feature_found = feature
break
if not feature_found:
print(f" ⚠️ Feature {feature_id} in runtime-status.json not found in backlog")
return
# Check if status matches
runtime_state = status.get('state')
backlog_status = feature_found.get('status')
if backlog_status == 'pending' and runtime_state in ['running', 'done']:
print(f" 🔧 Syncing runtime-status.json: {feature_id} is pending in backlog but {runtime_state} in runtime")
status['state'] = 'waiting'
status['stage'] = 'idle'
status['action'] = 'Feature is pending in backlog'
RUNTIME_STATUS.write_text(json.dumps(status, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
print(f" ✅ Synced runtime-status.json for {feature_id}")
else:
print(f" ✅ runtime-status.json is consistent with backlog")
except Exception as e:
print(f" ❌ Error syncing runtime-status.json: {e}")
def main():
print("🔧 Fixing Orquestra violations in new-mercadodevida\n")
# Load backlog
if not BACKLOG_PATH.exists():
print(f"❌ Backlog not found: {BACKLOG_PATH}")
return 1
try:
backlog = json.loads(BACKLOG_PATH.read_text(encoding='utf-8'))
except Exception as e:
print(f"❌ Invalid backlog: {e}")
return 1
# 1. Archive specs/ directory
print("1⃣ Archiving specs/ (plural) directory...")
fix_specs_directory()
print()
# 2. Fix features without valid gates
print("2⃣ Fixing features without valid gates...")
fixed_gates = fix_features_without_valid_gates(backlog)
print(f" ✅ Moved {len(fixed_gates)} features to pending: {', '.join(fixed_gates[:10])}{'...' if len(fixed_gates) > 10 else ''}")
print()
# 3. Fix features without artifacts
print("3⃣ Fixing features without artifacts...")
fixed_artifacts = fix_features_without_artifacts(backlog)
print(f" ✅ Moved {len(fixed_artifacts)} features to pending: {', '.join(fixed_artifacts[:10])}{'...' if len(fixed_artifacts) > 10 else ''}")
print()
# 4. Normalize gate nomenclature
print("4⃣ Normalizing gate nomenclature (review → reviewer)...")
normalized = normalize_gate_nomenclature(backlog)
print(f" ✅ Normalized {len(normalized)} features: {', '.join(normalized[:10])}{'...' if len(normalized) > 10 else ''}")
print()
# 5. Add completed_at to done features
print("5⃣ Adding completed_at to done features...")
fixed_timestamps = add_completed_at_to_done_features(backlog)
print(f" ✅ Added timestamps to {len(fixed_timestamps)} features")
print()
# 6. Standardize feature schema
print("6⃣ Standardizing feature schema...")
standardized = standardize_feature_schema(backlog)
print(f" ✅ Standardized {len(standardized)} features")
print()
# 7. Sync runtime-status.json
print("7⃣ Syncing runtime-status.json with backlog...")
sync_runtime_status(backlog)
print()
# Save backlog
try:
BACKLOG_PATH.write_text(json.dumps(backlog, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
print("✅ Saved updated backlog")
except Exception as e:
print(f"❌ Cannot save backlog: {e}")
return 1
# Summary
print("\n📊 Summary:")
print(f" - Features moved to pending (no valid gates): {len(fixed_gates)}")
print(f" - Features moved to pending (no artifacts): {len(fixed_artifacts)}")
print(f" - Features normalized (review → reviewer): {len(normalized)}")
print(f" - Features with added timestamps: {len(fixed_timestamps)}")
print(f" - Features standardized: {len(standardized)}")
print("\n✅ All fixes applied successfully")
return 0
if __name__ == '__main__':
import sys
sys.exit(main())

View File

@@ -79,16 +79,39 @@ append_gitignore_block() {
local gitignore=$1
touch "$gitignore"
if ! grep -q '^# BEGIN ORQUESTRA$' "$gitignore"; then
cat >>"$gitignore" <<'EOF'
cat >>"$gitignore" <<'INNEREOF'
# BEGIN ORQUESTRA
__pycache__/
*.pyc
.pytest_cache/
.codegraph/
.atl/
# Build artifacts
.next/
dist/
build/
*.log
# Dependencies
node_modules/
# Environment
.env
.env.local
.env.*.local
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# END ORQUESTRA
EOF
INNEREOF
fi
}
@@ -124,8 +147,9 @@ done
copy_dir_update "$SOURCE_DIR/harness" "$TARGET_DIR/harness"
copy_dir_update "$SOURCE_DIR/platforms/pi" "$TARGET_DIR/platforms/pi"
copy_dir_update "$SOURCE_DIR/docs" "$TARGET_DIR/docs"
for file in scripts/verify.sh scripts/agent_status.py scripts/new_ticket.py scripts/pi_orquestra.sh scripts/run_stage.py scripts/install.sh; do
for file in scripts/verify.sh scripts/agent_status.py scripts/new_ticket.py scripts/pi_orquestra.sh scripts/run_stage.py scripts/commit_feature.sh scripts/close_feature.py scripts/install.sh; do
copy_file_update "$SOURCE_DIR/$file" "$TARGET_DIR/$file"
done

View File

@@ -19,7 +19,7 @@ def parse_stages():
for raw in WORKFLOW.read_text(encoding='utf-8').splitlines():
line = raw.rstrip()
if line.startswith(' - name:'):
current = {'input': [], 'output': []}
current = {'input': [], 'output': [], 'post_actions': []}
current['name'] = line.split(':', 1)[1].strip()
stages[current['name']] = current
section = None
@@ -32,6 +32,8 @@ def parse_stages():
section = 'input'
elif line.startswith(' output:'):
section = 'output'
elif line.startswith(' post_actions:'):
section = 'post_actions'
elif section and line.startswith(' - '):
current[section].append(line.split('- ', 1)[1].strip())
elif line.startswith(' '):
@@ -46,7 +48,16 @@ def expand(paths, feature_id):
def prompt_for(stage, feature_id):
inputs = expand(stage.get('input', []), feature_id)
outputs = expand(stage.get('output', []), feature_id)
post_actions = expand(stage.get('post_actions', []), feature_id)
owner = stage.get('owner', 'leader')
post_actions_text = ""
if post_actions:
post_actions_text = f"""
Post-actions (execute after completing the stage):
{chr(10).join(f'- {a}' for a in post_actions)}
"""
return f"""You are the Orquestra stage agent '{owner}' for feature '{feature_id}'.
Fresh-process rule: do not rely on previous chat/session context. Use only the files listed here, explicit Engram memories you choose after a narrow search, and repository evidence you read yourself.
@@ -62,12 +73,14 @@ Stage input paths:
Stage output paths:
{chr(10).join(f'- {p}' for p in outputs) or '- none'}
{post_actions_text}
Rules:
- Before work, run: python3 scripts/agent_status.py set --feature-id {feature_id} --stage {stage['name']} --agent {owner} --state running --action "Running {stage['name']}"
- Read only the stage inputs that exist. If a required input is missing, write a blocked artifact when possible and answer blocked -> <path>.
- Do not carry or summarize previous chat. Previous stage context is on disk in the declared input artifacts.
- Write evidence to the declared output path(s). Do not return code in chat.
- NEVER edit backlog/features.json directly. Use scripts/close_feature.py to close features.
- Before finishing, save to Engram only if this stage produced durable knowledge: a decision, bugfix, non-obvious discovery, reusable convention, or configuration change. Do not save routine progress, command output, or artifact summaries; those belong in the output artifact.
- Finish by updating runtime status to done or blocked.
- Final response must be exactly: done -> <path> or blocked -> <path>.
"""

View File

@@ -24,6 +24,7 @@ required=(
"harness/contracts/handoff.md"
"harness/contracts/evidence.schema.json"
"harness/model-routing.yml"
"docs/context-handoff.md"
"spec/product.md"
"spec/tech.md"
"spec/acceptance.md"
@@ -36,6 +37,8 @@ required=(
"scripts/new_ticket.py"
"scripts/pi_orquestra.sh"
"scripts/run_stage.py"
"scripts/commit_feature.sh"
"scripts/close_feature.py"
"platforms/pi/README.md"
"platforms/pi/extensions/orquestra-status/index.ts"
"platforms/pi/extensions/orquestra-web-fetch.ts"
@@ -60,6 +63,16 @@ for d in "${required_dirs[@]}"; do
fi
done
# Check for prohibited directories
prohibited_dirs=("specs" "apps" "frontend" "src" "lib")
for d in "${prohibited_dirs[@]}"; do
if [ -d "$d" ]; then
fail "Directorio prohibido encontrado: $d/ (usar project/ para código)"
EXIT_CODE=1
fi
done
ok "Sin directorios prohibidos (specs/, apps/, frontend/, src/, lib/)"
root_product_files=$(find . -mindepth 1 -maxdepth 1 -type f \( \
-name '*.py' -o -name '*.js' -o -name '*.ts' -o -name '*.go' -o -name '*.rs' -o \
-name '*.java' -o -name '*.php' -o -name '*.rb' \
@@ -137,14 +150,39 @@ if len(in_progress) > 1:
print(f"[FAIL] Hay {len(in_progress)} features in_progress (máximo 1)")
sys.exit(1)
# Required fields for feature schema
required_fields = ['id', 'status']
# Valid gate names
valid_gate_names = {'reviewer', 'security', 'qa', 'close', 'leader'}
for f in features:
fid = str(f.get('id', '')).strip()
status = f.get('status')
# Check required fields
for field in required_fields:
if field not in f:
print(f"[FAIL] Feature {fid} missing required field: {field}")
sys.exit(1)
if status not in valid:
print(f"[FAIL] Estado inválido en feature {fid}: {status}")
sys.exit(1)
# Validate gate nomenclature
gates = f.get('gates', {})
if 'review' in gates:
print(f"[FAIL] Feature {fid} uses deprecated gate name 'review' (should be 'reviewer')")
sys.exit(1)
# Check for invalid gate names
for gate_name in gates.keys():
if gate_name not in valid_gate_names:
print(f"[FAIL] Feature {fid} has invalid gate name: {gate_name}")
sys.exit(1)
if status == 'done':
# Check artifacts exist
d = root / 'work' / 'artifacts' / fid
req = ['reviewer.json', 'security.json', 'qa.json', 'leader-close.json']
missing = [name for name in req if not (d / name).is_file()]
@@ -152,6 +190,11 @@ for f in features:
print(f"[FAIL] Feature {fid} done sin artefactos: {', '.join(missing)}")
sys.exit(1)
# Check gates in backlog match artifacts
if not gates.get('reviewer') or not gates.get('security') or not gates.get('qa'):
print(f"[FAIL] Feature {fid} done sin gates aprobados en backlog")
sys.exit(1)
expected = {
'reviewer.json': 'reviewer',
'security.json': 'security',
@@ -188,6 +231,24 @@ if not isinstance(runtime.get('timeline'), list):
print('[FAIL] work/runtime-status.json timeline debe ser una lista')
sys.exit(1)
# Check runtime consistency with backlog
runtime_feature_id = runtime.get('feature_id')
if runtime_feature_id:
feature_in_backlog = None
for f in features:
if f.get('id') == runtime_feature_id:
feature_in_backlog = f
break
if feature_in_backlog:
runtime_state = runtime.get('state')
backlog_status = feature_in_backlog.get('status')
# If feature is pending in backlog but running/done in runtime, that's inconsistent
if backlog_status == 'pending' and runtime_state in ['running', 'done']:
print(f"[FAIL] Inconsistencia: {runtime_feature_id} está pending en backlog pero {runtime_state} en runtime-status")
sys.exit(1)
print(f"[OK] backlog válido ({len(features)} features)")
print('[OK] runtime-status válido')
PY