feat(ADM-018): completed feature
This commit is contained in:
@@ -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
119
scripts/close_feature.py
Executable 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
58
scripts/commit_feature.sh
Executable 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
|
||||
248
scripts/fix_orquestra_violations.py
Executable file
248
scripts/fix_orquestra_violations.py
Executable 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())
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>.
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user