#!/usr/bin/env python3 import argparse import os import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] WORKFLOW = ROOT / 'harness' / 'workflow.stages.yml' STATUS_EXT = ROOT / '.pi' / 'extensions' / 'orquestra-status' WEB_FETCH_EXT = ROOT / '.pi' / 'extensions' / 'orquestra-web-fetch.ts' ENGRAM_EXT = Path.home() / '.pi' / 'agent' / 'npm' / 'node_modules' / 'gentle-engram' / 'index.ts' def parse_stages(): stages = {} current = None section = None for raw in WORKFLOW.read_text(encoding='utf-8').splitlines(): line = raw.rstrip() if line.startswith(' - name:'): current = {'input': [], 'output': [], 'post_actions': []} current['name'] = line.split(':', 1)[1].strip() stages[current['name']] = current section = None continue if current is None: continue if line.startswith(' owner:'): current['owner'] = line.split(':', 1)[1].strip() elif line.startswith(' input:'): 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(' '): section = None return stages def expand(paths, feature_id): return [p.replace('', feature_id) for p in paths] 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. First read these harness rules: - AGENTS.md - harness/agents.matrix.yml - harness/workflow.stages.yml - harness/contracts/handoff.md Stage input paths: {chr(10).join(f'- {p}' for p in inputs) or '- none'} 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 -> . - 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 -> or blocked -> . """ def main(): parser = argparse.ArgumentParser(description='Run one Orquestra stage in a fresh Pi process with minimal declared context.') parser.add_argument('stage') parser.add_argument('--feature-id', required=True) parser.add_argument('--dry-run', action='store_true') args = parser.parse_args() stages = parse_stages() if args.stage not in stages: print(f"[FAIL] Unknown stage: {args.stage}", file=sys.stderr) print(f"Allowed: {', '.join(stages)}", file=sys.stderr) return 2 cmd = [ 'pi', '--no-session', '--no-context-files', '--no-extensions', '--no-skills', '-e', str(ENGRAM_EXT), '-e', str(STATUS_EXT), '-e', str(WEB_FETCH_EXT), '-p', prompt_for(stages[args.stage], args.feature_id), ] if args.dry_run: print(' '.join(cmd[:-1])) print('\n--- prompt ---') print(cmd[-1]) return 0 missing = [] if not ENGRAM_EXT.is_file(): missing.append(str(ENGRAM_EXT)) if not (STATUS_EXT / 'index.ts').is_file(): missing.append(str(STATUS_EXT / 'index.ts')) if not WEB_FETCH_EXT.is_file(): missing.append(str(WEB_FETCH_EXT)) if missing: print('[FAIL] Missing Pi runtime extension(s):', file=sys.stderr) for path in missing: print(f' - {path}', file=sys.stderr) print('Run scripts/install.sh for this project and install gentle-engram.', file=sys.stderr) return 1 return subprocess.run(cmd, cwd=ROOT, env=os.environ.copy()).returncode if __name__ == '__main__': raise SystemExit(main())