Spaces:
Running
Running
File size: 5,839 Bytes
95597da 2be3a97 95597da 2be3a97 95597da 2be3a97 95597da 2be3a97 95597da 2be3a97 95597da | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | """Build portable skills and WorkBuddy packages from one canonical skill."""
import argparse, hashlib, json, re, struct, zipfile
from pathlib import Path, PurePosixPath
ROOT = Path(__file__).resolve().parents[1]
VERSION = '0.2.0'
SKILL = 'editorial-guard-zh'
def text_bytes(path):
# Canonical LF makes package metadata independent of checkout platform.
return path.read_text(encoding='utf-8').encode('utf-8')
def skill_files(workbuddy=False):
files = {p.relative_to(ROOT/'skills'/SKILL).as_posix(): text_bytes(p)
for p in sorted((ROOT/'skills'/SKILL).rglob('*')) if p.is_file()}
files['LICENSE'] = text_bytes(ROOT/'LICENSE')
if workbuddy:
files = {n: v for n,v in files.items() if not n.startswith('agents/')}
text = files['SKILL.md'].decode('utf-8')
_, front, body = text.split('---', 2)
front += ('display_name: 中文改稿与交付检查\ndisplay_name_en: Chinese Editing and Delivery Review\n'
'description_zh: 润色已有中文草稿并保护原意与结构,宿主自查不等于独立验收。\n'
'description_en: Polish Chinese drafts and preserve source meaning; host review is not independent validation.\n'
'category: writing\nversion: "'+VERSION+'"\nauthor: Song Luo\n')
body = re.sub(r'\[([^\]]+)\]\((references/[^)]+)\)', r'\1:@\2', body)
files['SKILL.md'] = ('---'+front+'---'+body).encode('utf-8')
return files
def write_zip(destination, files):
with zipfile.ZipFile(destination,'w',zipfile.ZIP_DEFLATED,compresslevel=9) as archive:
for name, data in sorted(files.items()):
path = PurePosixPath(name)
if path.is_absolute() or '..' in path.parts or '\\' in name: raise ValueError('Unsafe ZIP entry')
info=zipfile.ZipInfo(name,(2026,1,1,0,0,0));info.compress_type=zipfile.ZIP_DEFLATED;info.external_attr=0o100644<<16
archive.writestr(info,data)
with zipfile.ZipFile(destination) as archive:
if archive.testzip() or len(archive.namelist())!=len(set(archive.namelist())): raise ValueError('Invalid archive')
def check_avatar(data):
if not data.startswith(b'\x89PNG\r\n\x1a\n') or struct.unpack('>II',data[16:24])!=(512,512) or len(data)>500_000:
raise ValueError('Expected 512 x 512 PNG under 500 KB')
def plugin_files(kind):
folder=ROOT/'platforms/workbuddy'/('editorial-guard-'+kind)
files={p.relative_to(folder).as_posix():text_bytes(p) for p in sorted(folder.rglob('*')) if p.is_file()}
meta=json.loads(files['.codebuddy-plugin/plugin.json'])
ids=[PurePosixPath(a).stem for a in meta['agents']]
if meta['agentName'] not in ids or meta['defaultInitPrompt']!=meta['quickPrompts'][0]:raise ValueError('Invalid entrypoint')
if len(meta['tags'])!=3 or len(meta['quickPrompts'])!=3:raise ValueError('Expected three tags and prompts')
if not 40<=len(re.findall('[\u4e00-\u9fff]',meta['displayDescription']['zh']))<=50:raise ValueError('Description length')
for agent in meta['agents']:
text=files[agent.removeprefix('./')].decode('utf-8')
if '\ntools:' in text or '\nname: '+PurePosixPath(agent).stem+'\n' not in text:raise ValueError('Invalid agent')
avatar_names=[PurePosixPath(meta['avatar']).name]
if kind=='team':
members=meta['members'];team=meta['teamInfo']
if {x['id'] for x in members}!=set(ids) or set(team['memberAgents'])!=set(ids)-{team['leadAgent']}:raise ValueError('Team roles differ')
if sum(x['role']=='lead' for x in members)!=1 or meta['agentName']!=team['leadAgent']:raise ValueError('Invalid team lead')
if files['settings.json']!=files['setting.json'] or json.loads(files['settings.json'])['agent']!=team['leadAgent']:raise ValueError('Settings differ')
avatar_names += [PurePosixPath(x['avatar']).name for x in members]
for name in avatar_names:
data=(ROOT/'platforms/workbuddy/avatars'/name).read_bytes();check_avatar(data);files['avatars/'+name]=data
files.update({'skills/'+SKILL+'/'+name:data for name,data in skill_files(True).items()})
files['LICENSE']=text_bytes(ROOT/'LICENSE')
for path in meta['skills']:
if path.removeprefix('./')+'/SKILL.md' not in files:raise ValueError('Missing embedded skill')
return {folder.name+'/'+name:data for name,data in files.items()}
def build(output):
out=Path(output).resolve()
if out==ROOT or ROOT in out.parents:raise ValueError('Use output outside source repository')
if out.exists() and any(out.iterdir()):raise ValueError('Output must be empty')
out.mkdir(parents=True,exist_ok=True)
for suffix,files in [
('editorial-guard-zh', {SKILL+'/'+n:v for n,v in skill_files().items()}),
('editorial-guard-zh-portable-flat',skill_files()),
('editorial-guard-workbuddy-skill',skill_files(True)),
('editorial-guard-workbuddy-expert',plugin_files('expert')),
('editorial-guard-workbuddy-team',plugin_files('team'))]:
write_zip(out/(suffix+'-v'+VERSION+'.zip'),files)
avatar=(ROOT/'platforms/workbuddy/avatars/skill.png').read_bytes();check_avatar(avatar)
(out/'editorial-guard-skill-avatar.png').write_bytes(avatar)
report={'version':VERSION,'local_package_checks':'pass','platform_import_tested':False,
'portable_flat_contract':'SKILL.md at ZIP root; no platform-specific manifest asserted',
'files':{p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(out.iterdir()) if p.is_file()}}
(out/'platform-package-manifest.json').write_text(json.dumps(report,indent=2)+'\n',encoding='utf-8')
return report
if __name__=='__main__':
parser=argparse.ArgumentParser();parser.add_argument('--output',required=True)
print(json.dumps(build(parser.parse_args().output),indent=2))
|