Files
zkxw/.agents/skills/personal-dev/scripts/pda/issue_queue.py
T
zlwu 42301730ef chore(agents): 更新个人智能体至官方 main eb4c320
固定协议 6 安装内容与来源摘要,保留试点工程配置及既有授权,为合入 V2.0 提供可复现版本。
2026-11-02 11:59:53 +08:00

603 lines
31 KiB
Python

"""Session-operated queue control; never executes issue text or business commands."""
from contextlib import contextmanager
from datetime import datetime
import fcntl
import json
import hashlib
import os
from pathlib import Path
import re
import subprocess
import uuid
from .transaction import InstallError, atomic_write, json_bytes, read, safe_path
DIRECTORY = '.local/agent/queue'
STATE = DIRECTORY + '/state.json'
STOP = DIRECTORY + '/stop'
LOCK = DIRECTORY + '/write.lock'
REF = 'refs/heads/personal-dev/queue-lock'
STAGES = {'claimed', 'planning', 'implementing', 'testing', 'waiting_user', 'reviewing', 'ready_to_merge'}
_EXPLICIT_START = object()
REASONS = {'needs_input', 'dependency', 'test_failed', 'auth', 'environment', 'review'}
def git(root, *args, data=None):
env = dict(os.environ, GIT_TERMINAL_PROMPT='0', GIT_AUTHOR_NAME='Personal Dev Queue',
GIT_AUTHOR_EMAIL='queue@example.invalid', GIT_COMMITTER_NAME='Personal Dev Queue',
GIT_COMMITTER_EMAIL='queue@example.invalid')
try:
result = subprocess.run(['git', '-C', str(root), *args], input=data,
capture_output=True, text=True, timeout=25, env=env)
except (OSError, subprocess.TimeoutExpired) as exc:
raise InstallError('queue Git unavailable or timed out; inspect ownership before retry') from exc
if result.returncode:
raise InstallError('queue Git operation failed; verify authentication, branch and ownership')
return result.stdout.strip()
class QueueConflict(InstallError):
"""Requires reconciliation, not an automatic network retry."""
pass
class QueueBusy(InstallError):
pass
class GitLease:
"""Distinct orphan commit plus expected-ref CAS, not an advisory issue comment."""
def __init__(self, root, repository=None):
self.root, self.repository = root, repository
def _validate(self):
if self.repository: validate_remote(self.root, self.repository)
def current(self):
self._validate()
value = git(self.root, 'ls-remote', '--refs', 'origin', REF)
if not value: return None
fields = value.split()
if len(fields) != 2 or fields[1] != REF or not re.fullmatch('[0-9a-f]{40}', fields[0]):
raise InstallError('invalid queue ownership response')
return fields[0]
def prepare(self, session):
blob = git(self.root, 'hash-object', '-w', '--stdin', data=json.dumps({'session': session, 'purpose': 'personal-dev-queue'}))
tree = git(self.root, 'mktree', data='100644 blob ' + blob + '\tqueue.json\n')
return git(self.root, 'commit-tree', tree, data='Personal dev queue ownership ' + session + '\n')
def acquire(self, sha):
self._validate()
git(self.root, 'push', '--porcelain', '--force-with-lease=' + REF + ':', 'origin', sha + ':' + REF)
if self.current() != sha:
raise InstallError('queue ownership acquisition not confirmed')
def release(self, sha):
current = self.current()
if current is None: return
if current != sha: raise QueueConflict('queue ownership belongs to another session; do not delete it')
git(self.root, 'push', '--porcelain', '--force-with-lease=' + REF + ':' + sha, 'origin', ':' + REF)
if self.current() is not None:
raise InstallError('queue ownership release not confirmed')
def contains(self, commit, branch):
self._validate()
git(self.root, 'check-ref-format', '--branch', branch)
if branch.startswith('-'): raise InstallError('invalid integration branch')
ref = 'refs/heads/' + branch
fields = git(self.root, 'ls-remote', '--refs', 'origin', ref).split()
if len(fields) != 2 or fields[1] != ref or not re.fullmatch('[0-9a-f]{40}', fields[0]):
raise InstallError('integration branch snapshot unavailable')
target = fields[0]
git(self.root, 'fetch', '--no-tags', '--no-write-fetch-head', 'origin', target)
try:
git(self.root, 'merge-base', '--is-ancestor', commit, target)
return True
except InstallError:
return False
def main_project(root):
"""Git's first worktree is the common/main checkout; do not scan other projects."""
records = git(root, 'worktree', 'list', '--porcelain', '-z').split('\0')
if not records or not records[0].startswith('worktree '):
raise InstallError('cannot identify main project worktree')
main = Path(records[0][len('worktree '):]).resolve()
if not main.is_dir(): raise InstallError('main project worktree is unavailable')
return main
def labels(item):
return {x['name'].lower() for x in item.get('labels', []) if isinstance(x, dict) and isinstance(x.get('name'), str)}
def eligible(item):
names = labels(item)
return (type(item.get('number')) is int and item['number'] > 0 and item.get('state') == 'open'
and item.get('pull_request') is None and 'ai-ready' in names
and not names.intersection({'ai-blocked', 'ai-in-progress'})
and not item.get('assignees') and not item.get('assignee'))
def order(item):
names = labels(item)
priority = min((i for i in range(4) if 'p' + str(i) in names), default=4)
try:
created = datetime.fromisoformat(item['created_at'].replace('Z', '+00:00'))
if created.tzinfo is None: raise ValueError()
except (KeyError, AttributeError, TypeError, ValueError):
raise InstallError('valid timezone-aware issue creation timestamp required') from None
return priority, created, item['number']
class Queue:
def __init__(self, root, tracker=None, lease=None, branch=None, owner=None):
self.root, self.tracker = Path(root), tracker
self.lease = lease or GitLease(root)
self.branch, self.owner = branch, owner
def _directory(self):
safe_path(self.root, STATE)
current = self.root
for part in DIRECTORY.split('/'):
current = current / part
current.mkdir(mode=0o700, exist_ok=True)
current.chmod(0o700)
@contextmanager
def _locked(self):
self._directory()
path = safe_path(self.root, LOCK)
fd = os.open(path, os.O_RDWR | os.O_CREAT, 0o600)
with os.fdopen(fd, 'a+b') as handle:
try: fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError as exc:
raise QueueBusy('another queue command is running; stop request remains effective') from exc
try: yield
finally: fcntl.flock(handle, fcntl.LOCK_UN)
def status(self):
raw = read(self.root, STATE)
if raw is None:
state = dict(version=1, enabled=False, session=None, repository=None, integration_branch=None,
lease=None, owner=None, current=None, blocked={}, completed=[], release='released', stop_ack=None)
else:
try: state = json.loads(raw)
except (ValueError, UnicodeError) as exc: raise InstallError('invalid queue state; preserve it for recovery') from exc
if (not isinstance(state, dict) or state.get('version') != 1 or type(state.get('enabled')) is not bool
or not isinstance(state.get('blocked'), dict) or not isinstance(state.get('completed'), list)):
raise InstallError('invalid queue state; preserve it for recovery')
if self._stopped(state):
state['enabled'] = False
if state.get('lease'):
state['release'] = 'retained_for_resume' if state.get('current') else 'pending'
return state
def _stop_token(self):
raw = read(self.root, STOP)
return hashlib.sha256(raw).hexdigest() if raw is not None else None
def _stopped(self, state):
return self._stop_token() != state.get('stop_ack')
def _save(self, state):
if self._stopped(state): state['enabled'] = False
atomic_write(self.root, STATE, json_bytes(state), 0o600)
def _transition(self, state):
# Called while holding the queue lock. Stop latches independently; if it
# races the replace, restore the previous checkpoint before releasing lock.
if self._stopped(state): raise InstallError('stop requested; queue transition rejected')
before = read(self.root, STATE)
atomic_write(self.root, STATE, json_bytes(state), 0o600)
if self._stopped(state):
atomic_write(self.root, STATE, before, 0o600)
raise InstallError('stop requested; previous task progress retained')
def _owner(self, state):
if not self.owner or state.get('owner') != self.owner:
raise QueueConflict('queue belongs to a different active conversation')
def _active(self, state):
self._owner(state)
if not state['enabled'] or self._stopped(state):
raise InstallError('queue is stopped; only explicit user start may resume')
if (not self.tracker or state['repository'] != self.tracker.repository
or state['integration_branch'] != self.branch):
raise QueueConflict('queue repository or integration branch changed; reconcile saved work first')
if not state['lease'] or self.lease.current() != state['lease']:
raise QueueConflict('queue ownership missing or changed; stop work')
if self._stopped(state):
raise InstallError('stop requested; do not advance')
def start(self, expected_stop=_EXPLICIT_START):
acknowledged_stop = self._stop_token() if expected_stop is _EXPLICIT_START else expected_stop
with self._locked():
state = self.status()
if self._stop_token() != acknowledged_stop:
raise InstallError('stop requested before start; authorization revoked')
if not self.tracker or not self.branch: raise InstallError('configured tracker and integration branch required')
if not self.owner or not re.fullmatch(r'[A-Za-z0-9_-]{1,100}', self.owner):
raise InstallError('explicit conversation owner ID required')
if state['enabled'] and state.get('owner') != self.owner:
raise QueueConflict('another conversation is running; stop it before transferring ownership')
if state['repository'] is not None and (state['repository'] != self.tracker.repository or state['integration_branch'] != self.branch):
raise QueueConflict('queue binding changed; reconcile saved work before starting')
# Acknowledge only the stop observed on entry; never delete a newer stop.
state.update(repository=self.tracker.repository, integration_branch=self.branch,
enabled=False, stop_ack=acknowledged_stop, owner=self.owner)
if state['session'] is None: state['session'] = str(uuid.uuid4())
current = self.lease.current()
if current is not None and current != state['lease']:
raise QueueConflict('repository queue is occupied; do not steal or expire its lock')
if current is None:
state['lease'] = self.lease.prepare(state['session'] + '-' + str(uuid.uuid4()))
state['release'] = 'pending'
self._save(state) # Save identity BEFORE an ambiguous push or process exit.
if self._stopped(state): raise InstallError('stop requested during start')
self.lease.acquire(state['lease'])
state.update(enabled=True, release='owned')
self._save(state)
return self.status()
def stop(self):
self._directory()
atomic_write(self.root, STOP, (str(uuid.uuid4()) + '\n').encode(), 0o600) # Does not wait for a long request.
try:
with self._locked():
state = self.status()
if isinstance(self.lease, GitLease):
self.lease.repository = state.get('repository')
state['enabled'] = False
if state['current'] is not None:
state['release'] = 'retained_for_resume'
self._save(state)
return state
return self._release(state)
except QueueBusy:
state = self.status()
state['cleanup_required'] = True
return state
def _release(self, state):
state['enabled'] = False
self._save(state)
try:
if state['lease']: self.lease.release(state['lease'])
except InstallError:
state['release'] = 'pending'
else:
state.update(lease=None, release='released')
self._save(state)
return state
def next(self):
# A stopped no-op must not even create a state directory.
if not self.status()['enabled']: raise InstallError('queue is stopped')
with self._locked():
state = self.status(); self._active(state)
if state['current'] is not None:
item = self.tracker.issue(state['current']['number'])
self._active(state)
if not eligible(item):
raise QueueConflict('current issue closed, withdrawn or assigned; reconcile before continuing')
return {'status': 'resume', 'issue': item, 'checkpoint': state['current']}
items = self.tracker.issues()
self._active(state)
candidates = sorted((x for x in items if eligible(x) and str(x['number']) not in state['blocked']
and x['number'] not in state['completed']), key=order)
for candidate in candidates:
item = self.tracker.issue(candidate['number']) # Refetch description + comments before claiming.
self._active(state)
if not eligible(item): continue
state['current'] = dict(number=item['number'], stage='claimed', branch=None, worktree=None,
commit=None, updated_at=item.get('updated_at'))
self._transition(state)
if not self.status()['enabled']: raise InstallError('stop requested; retain saved task for resume')
return {'status': 'claimed', 'issue': item, 'checkpoint': state['current']}
state = self._release(state)
return {'status': 'empty', 'queue': state}
def guard(self):
with self._locked():
state = self.status()
self._active(state)
if not state['current']: raise InstallError('no current queue issue')
return state
def cleanup_guard(self):
"""Authorize a queue boundary, not deletion of any particular worktree."""
with self._locked():
state = self.status()
self._active(state)
if state['current'] is not None:
raise InstallError('current issue still exists; defer worktree cleanup until after queue-finish')
return state
def checkpoint(self, stage, branch=None, worktree=None, commit=None):
if stage not in STAGES: raise InstallError('unknown queue stage')
if commit is not None and not re.fullmatch('[0-9a-f]{40}', commit): raise InstallError('full commit SHA required')
for value in (branch, worktree):
if value is not None and (not value or not value.isprintable() or len(value) > 1000):
raise InstallError('invalid checkpoint locator')
with self._locked():
state = self.status()
if not state['current']: raise InstallError('no current queue issue')
self._owner(state)
# Safe bookkeeping remains available after stop, without external actions.
state['current']['stage'] = stage
for name, value in (('branch', branch), ('worktree', worktree), ('commit', commit)):
if value is not None: state['current'][name] = value
self._save(state)
return state
def block(self, reason):
if reason not in REASONS: raise InstallError('unknown blocking reason')
with self._locked():
state = self.status(); self._active(state)
if not state['current']: raise InstallError('no current queue issue')
item = self.tracker.issue(state['current']['number'])
self._active(state)
if 'ai-blocked' not in labels(item):
raise InstallError('record reason and ai-blocked on the remote issue before skipping it')
state['blocked'][str(item['number'])] = dict(state['current'], reason=reason)
state['current'] = None
self._transition(state)
return state
def retry(self, number):
if type(number) is not int or number <= 0: raise InstallError('positive issue number required')
with self._locked():
state = self.status(); self._active(state)
if state['current']: raise InstallError('finish or pause the current issue first')
if str(number) not in state['blocked'] and number not in state['completed']:
raise InstallError('issue is not a previously blocked or completed queue task')
item = self.tracker.issue(number)
self._active(state)
if not eligible(item): raise InstallError('restore open ai-ready and clear blocking/assignment before retry')
state['current'] = state['blocked'].pop(str(number), dict(number=number, stage='claimed',
branch=None, worktree=None, commit=None, updated_at=item.get('updated_at')))
state['current'].pop('reason', None)
if number in state['completed']: state['completed'].remove(number)
self._transition(state)
return state
def finish(self, commit, evidence):
if not re.fullmatch('[0-9a-f]{40}', commit or ''): raise InstallError('full merge commit required')
if not evidence or not evidence.isprintable() or len(evidence) > 1000: raise InstallError('evidence locator required')
with self._locked():
state = self.status(); self._active(state)
if not state['current']: raise InstallError('no current queue issue')
number = state['current']['number']
Delivery(self.root, self.tracker.repository, self.branch).check(number, commit, self.tracker, self.lease)
self._active(state)
state['completed'].append(number)
state['last_completion'] = dict(number=number, commit=commit, evidence=evidence)
state['current'] = None
self._transition(state)
return state
DELIVERY_DIRECTORY = '.local/agent/delivery'
DELIVERY_STEPS = ('accepted', 'integration_verified', 'remote_synced', 'environment_restored', 'issue_updated')
class Delivery:
"""Local evidence checkpoints plus live completion checks, shared by single issues and queues.
Checkpoints are Agent assertions, not test execution or external write authorization.
Only check() reads the remote; it never pushes, comments, closes issues or starts services.
"""
def __init__(self, root, repository, branch):
self.root, self.repository, self.branch = Path(root), repository, branch
def _path(self, number):
if type(number) is not int or number <= 0:
raise InstallError('positive issue number required')
return DELIVERY_DIRECTORY + '/' + str(number) + '.json'
@contextmanager
def _locked(self):
path = safe_path(self.root, DELIVERY_DIRECTORY + '/write.lock')
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
path.parent.chmod(0o700)
fd = os.open(path, os.O_RDWR | os.O_CREAT, 0o600)
with os.fdopen(fd, 'a+b') as handle:
try: fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError as exc:
raise InstallError('another delivery command is running') from exc
try: yield
finally: fcntl.flock(handle, fcntl.LOCK_UN)
def status(self, number):
raw = read(self.root, self._path(number))
if raw is None:
return {'status': 'unrecorded', 'issue': number, 'record': None, 'missing': list(DELIVERY_STEPS)}
try:
value = json.loads(raw)
if (not isinstance(value, dict) or value.get('version') != 1
or value.get('issue') != number
or value.get('repository') != self.repository
or value.get('integration_branch') != self.branch
or not isinstance(value.get('commit'), str)
or not re.fullmatch('[0-9a-f]{40}', value['commit'])
or not isinstance(value.get('steps'), dict)
or set(value['steps']) - set(DELIVERY_STEPS)
or any(not isinstance(v, str) or not v.strip() or not v.isprintable() or len(v) > 1000
for v in value['steps'].values())):
raise ValueError()
except (ValueError, TypeError, UnicodeError):
raise InstallError('invalid delivery record or repository/branch binding; preserve it for recovery') from None
return {'status': 'recorded', 'issue': number, 'record': value,
'missing': [step for step in DELIVERY_STEPS if step not in value['steps']],
'requires_live_check': True}
def _save(self, number, value):
atomic_write(self.root, self._path(number), json_bytes(value), 0o600)
def record(self, number, commit, stage, evidence):
self._path(number)
if not isinstance(commit, str) or not re.fullmatch('[0-9a-f]{40}', commit):
raise InstallError('full delivery commit required')
if stage not in (*DELIVERY_STEPS, 'waiting_user'):
raise InstallError('unknown delivery stage')
if not isinstance(evidence, str) or not evidence.strip() or not evidence.isprintable() or len(evidence) > 1000:
raise InstallError('evidence locator required; never store credentials or issue bodies')
with self._locked():
old = self.status(number)['record']
if old is None or old['commit'] != commit:
if old:
atomic_write(self.root, DELIVERY_DIRECTORY + '/archive/' + str(number) + '-' + old['commit'] + '.json',
json_bytes(old), 0o600)
value = dict(version=1, repository=self.repository, integration_branch=self.branch,
issue=number, commit=commit, steps={}, waiting=None, last_verified=None)
else:
value = old
if stage == 'waiting_user':
value.update(steps={}, waiting=evidence, last_verified=None)
elif value['steps'].get(stage) != evidence:
# New earlier evidence invalidates downstream claims; identical retries are no-ops.
index = DELIVERY_STEPS.index(stage)
value['steps'] = {k: v for k, v in value['steps'].items() if DELIVERY_STEPS.index(k) < index}
value['steps'][stage] = evidence
value.update(waiting=None, last_verified=None)
self._save(number, value)
return self.status(number)
def check(self, number, commit, tracker, lease):
with self._locked():
current = self.status(number)
value = current['record']
if value is None:
raise InstallError('missing delivery record; reconstruct evidence before finishing')
value['last_verified'] = None
self._save(number, value)
if value['commit'] != commit:
raise InstallError('delivery commit changed; revalidate and record final version')
if current['missing']:
raise InstallError('missing delivery evidence: ' + ', '.join(current['missing']))
if tracker.repository != self.repository:
raise InstallError('delivery tracker repository mismatch')
if not lease.contains(commit, self.branch):
raise InstallError('delivery commit is not reachable from remote integration branch')
item = tracker.issue(number)
if item.get('number') != number or item.get('state') != 'closed' or item.get('pull_request') is not None:
raise InstallError('remote issue must be closed and read back before completing delivery')
# A snapshot is never a permanent guarantee: later calls always re-read remote state.
value['last_verified'] = datetime.now().astimezone().isoformat()
self._save(number, value)
return {'status': 'verified', 'issue': number, 'repository': self.repository,
'integration_branch': self.branch, 'commit': commit, 'issue_state': 'closed',
'checked_at': value['last_verified']}
def validate_delivery_storage(root, delivery, number, next_commit=None):
"""Check every actual write target, not a representative filename."""
path = delivery._path(number)
if git(root, 'ls-files', '--', DELIVERY_DIRECTORY):
raise InstallError('delivery records must not be Git-tracked')
paths = [path, DELIVERY_DIRECTORY + '/write.lock']
old = delivery.status(number)['record']
if old and next_commit is not None and old['commit'] != next_commit:
paths.append(DELIVERY_DIRECTORY + '/archive/' + str(number) + '-' + old['commit'] + '.json')
for target in paths:
safe_path(root, target)
try:
git(root, 'check-ignore', '--quiet', '--', target)
except InstallError:
raise InstallError('delivery state, lock and archive targets must be Git-ignored') from None
def validate_remote(root, repository):
from .issue_tracker import canonical_repository
for flags in (('--all',), ('--push', '--all')):
urls = git(root, 'remote', 'get-url', *flags, 'origin').splitlines()
if len(urls) != 1 or canonical_repository(urls[0]) != repository:
raise InstallError('origin fetch/push URL must match the configured issue repository')
def configuration(root):
from .issue_tracker import canonical_repository
try:
config = json.loads(read(root, 'docs/agents/project.json') or b'{}')
tracker = config['project']['tracker']
repository = tracker['repository']
if not isinstance(repository, str) or not repository.startswith('https://'):
raise ValueError()
repository = canonical_repository(repository)
branch = config['project']['integration_branch']
if not isinstance(branch, str) or branch.startswith('-'): raise ValueError()
git(root, 'check-ref-format', '--branch', branch)
if tracker['kind'] not in ('gitea', 'github'): raise ValueError()
except (KeyError, TypeError, ValueError, UnicodeError):
raise InstallError('valid tracker repository and integration branch configuration required') from None
validate_remote(root, repository)
return config, tracker, repository, branch
def dispatch(root, args):
"""CLI boundary: resolve shared root, validate binding, then call a small state transition."""
root = main_project(root)
queue = Queue(root, owner=args.owner)
delivery_action = args.action.startswith('delivery-')
action = args.action.split('-', 1)[1]
if action == 'status' and not delivery_action: return queue.status()
if action == 'stop':
# Even malformed tracker config cannot prevent the local stop latch.
return queue.stop()
if action == 'checkpoint':
return queue.checkpoint(args.stage, args.branch, args.worktree, args.commit)
from .issue_tracker import Tracker
config, tracker, repository, branch = configuration(root)
if delivery_action:
delivery = Delivery(root, repository, branch)
if action == 'status':
if args.issue is not None:
return delivery.status(args.issue)
safe_path(root, DELIVERY_DIRECTORY + '/write.lock')
numbers = sorted(int(p.stem) for p in (root / DELIVERY_DIRECTORY).glob('*.json') if p.stem.isdigit())
return {'status': 'inventory', 'tasks': [delivery.status(n) for n in numbers]}
validate_delivery_storage(root, delivery, args.issue, args.commit if action == 'record' else None)
if action == 'record':
return delivery.record(args.issue, args.commit, args.stage, args.evidence)
# A queue task must use its guarded finish operation; standalone checking is not a queue bypass.
current = queue.status().get('current')
if current and current.get('number') == args.issue:
raise InstallError('current queue issue must use queue-finish with its owner')
permissions = config.get('permissions', {})
if action == 'start' and not args.apply:
return {'status': 'preview', 'project': str(root), 'repository': repository,
'integration_branch': branch, 'label': 'ai-ready', 'lock_ref': REF,
'effect': 'creates/deletes an orphan control branch; may trigger branch CI; no Tag or deployment',
'needs': ['explicit user start', 'Git write login', 'issue API access',
'recorded push_task_branch and write_issue permissions']}
if action == 'start':
if any(permissions.get(key) is not True for key in ('push_task_branch', 'write_issue')):
raise InstallError('record existing queue permissions for push_task_branch and write_issue before starting')
git(root, 'check-ignore', '.local/agent/queue/state.json')
if git(root, 'ls-files', '--', '.local/agent/queue'):
raise InstallError('queue state must not be Git-tracked')
credentials = config.get('environment', {}).get('secrets')
if credentials in (None, '', '.local/secrets/access.json', 'external-controlled-storage'):
credentials = None
if not delivery_action and action != 'start' and not queue.status()['enabled']:
raise InstallError('queue is stopped')
queue.tracker = Tracker(tracker['kind'], repository, root, credentials_path=credentials)
queue.lease = GitLease(root, repository=repository)
queue.branch = branch
if delivery_action:
return delivery.check(args.issue, args.commit, queue.tracker, queue.lease)
if action == 'start': return queue.start()
if action == 'next': return queue.next()
if action == 'guard': return queue.guard()
if action == 'cleanup-guard': return queue.cleanup_guard()
if action == 'block': return queue.block(args.reason)
if action == 'retry': return queue.retry(args.issue)
if action == 'finish':
current = queue.status().get('current')
if current:
validate_delivery_storage(root, Delivery(root, repository, branch), current['number'])
return queue.finish(args.commit, args.evidence)
raise InstallError('unknown queue action')