@esquadrao/cli 0.0.0-stage → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +366 -2
- package/assets/fonts/Anton-Regular.ttf +0 -0
- package/assets/fonts/BebasNeue-Regular.ttf +0 -0
- package/assets/fonts/OFL.txt +93 -0
- package/assets/fonts/Poppins-Bold.ttf +0 -0
- package/assets/fonts/README.md +13 -0
- package/bin/cli.js +126 -0
- package/package.json +58 -4
- package/scripts/ast_slice.js +73 -0
- package/scripts/cutout.py +197 -0
- package/scripts/dag_validator.js +78 -0
- package/scripts/lint.js +43 -0
- package/scripts/merkle_verify.js +83 -0
- package/scripts/wcag_check.js +77 -0
- package/skill-audit/SKILL.md +42 -0
- package/skill-audit/references/merkle-ledger-spec.md +27 -0
- package/skill-audit/references/receipt-ed25519-verification.md +17 -0
- package/skill-audit/references/time-travel-replay.md +10 -0
- package/skill-code-reviewer/SKILL.md +40 -0
- package/skill-context/SKILL.md +44 -0
- package/skill-context/references/ast-virtualization.md +10 -0
- package/skill-context/references/openapi-contract-synthesis.md +10 -0
- package/skill-engineering-manager/SKILL.md +40 -0
- package/skill-media-producer/SKILL.md +328 -0
- package/skill-media-producer/references/caption-templates.md +194 -0
- package/skill-media-producer/references/composicoes-especiais.md +99 -0
- package/skill-media-producer/references/configuracao-inicial.md +239 -0
- package/skill-media-producer/references/corte-podcast-3-niveis.md +179 -0
- package/skill-media-producer/references/llm-agent-guide.md +231 -0
- package/skill-orchestrate/SKILL.md +49 -0
- package/skill-orchestrate/references/mission-lifecycle.md +36 -0
- package/skill-orchestrate/references/swarm-coordination.md +33 -0
- package/skill-pipeline/SKILL.md +42 -0
- package/skill-pipeline/references/auto-healing-patterns.md +17 -0
- package/skill-pipeline/references/dag-decomposition-rules.md +18 -0
- package/skill-pipeline/references/topological-waves.md +16 -0
- package/skill-product-manager/SKILL.md +40 -0
- package/skill-qa-engineer/SKILL.md +40 -0
- package/skill-review/SKILL.md +46 -0
- package/skill-review/references/anti-slop-patterns.md +17 -0
- package/skill-review/references/dual-blind-adjudication.md +15 -0
- package/skill-review/references/voice-calibration.md +10 -0
- package/skill-security-engineer/SKILL.md +40 -0
- package/skill-software-architect/SKILL.md +40 -0
- package/skill-software-engineer/SKILL.md +40 -0
- package/skill-sre-engineer/SKILL.md +40 -0
- package/skill-system-analyst/SKILL.md +40 -0
- package/skill-tech-lead/SKILL.md +40 -0
- package/skill-technical-writer/SKILL.md +40 -0
- package/skill-ui/SKILL.md +45 -0
- package/skill-ui/references/design-tokens-spec.md +19 -0
- package/skill-ui/references/visual-tdd-methodology.md +10 -0
- package/skill-ui/references/wcag-target-sizes.md +9 -0
- package/src/captionTemplates.js +285 -0
- package/src/commands/onboard.js +618 -0
- package/src/commands/role.js +119 -0
- package/src/commands/setup.js +174 -0
- package/src/commands/studio.js +1822 -0
- package/src/roles/authority.js +129 -0
- package/src/roles/codeReviewer.js +65 -0
- package/src/roles/engineeringManager.js +53 -0
- package/src/roles/index.js +80 -0
- package/src/roles/mediaProducer.js +223 -0
- package/src/roles/productManager.js +50 -0
- package/src/roles/qaEngineer.js +54 -0
- package/src/roles/securityEngineer.js +53 -0
- package/src/roles/softwareArchitect.js +58 -0
- package/src/roles/softwareEngineer.js +76 -0
- package/src/roles/sreEngineer.js +60 -0
- package/src/roles/systemAnalyst.js +45 -0
- package/src/roles/techLead.js +46 -0
- package/src/roles/technicalWriter.js +46 -0
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@esquadrao/cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "Esquadrão CLI and autonomous skill orchestration framework for AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"esquadrao": "bin/cli.js",
|
|
8
|
+
"esquadrao-skill": "bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"assets",
|
|
12
|
+
"bin",
|
|
13
|
+
"src",
|
|
14
|
+
"scripts",
|
|
15
|
+
"skill-orchestrate",
|
|
16
|
+
"skill-pipeline",
|
|
17
|
+
"skill-review",
|
|
18
|
+
"skill-audit",
|
|
19
|
+
"skill-context",
|
|
20
|
+
"skill-ui",
|
|
21
|
+
"skill-product-manager",
|
|
22
|
+
"skill-system-analyst",
|
|
23
|
+
"skill-software-architect",
|
|
24
|
+
"skill-tech-lead",
|
|
25
|
+
"skill-security-engineer",
|
|
26
|
+
"skill-software-engineer",
|
|
27
|
+
"skill-code-reviewer",
|
|
28
|
+
"skill-sre-engineer",
|
|
29
|
+
"skill-qa-engineer",
|
|
30
|
+
"skill-technical-writer",
|
|
31
|
+
"skill-engineering-manager",
|
|
32
|
+
"skill-media-producer",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18.0.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"lint": "node scripts/lint.js",
|
|
40
|
+
"test": "node --test"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"esquadrao",
|
|
44
|
+
"esquadrao-de-conteudo",
|
|
45
|
+
"skills",
|
|
46
|
+
"agents",
|
|
47
|
+
"claude",
|
|
48
|
+
"grok",
|
|
49
|
+
"codex",
|
|
50
|
+
"cursor",
|
|
51
|
+
"orchestration",
|
|
52
|
+
"studio",
|
|
53
|
+
"cli"
|
|
54
|
+
],
|
|
55
|
+
"license": "MIT",
|
|
56
|
+
"dependencies": {},
|
|
57
|
+
"optionalDependencies": {
|
|
58
|
+
"@xenova/transformers": "^2.17.2"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
|
|
7
|
+
// ponytail: regex AST signature slicer; upgrade to tree-sitter for deep AST traversal
|
|
8
|
+
export function sliceAst(code = '', lang = 'auto') {
|
|
9
|
+
if (typeof code !== 'string') return { lang, declarations: [], outline: '' };
|
|
10
|
+
const detected = lang && lang !== 'auto'
|
|
11
|
+
? (lang.toLowerCase().includes('py') ? 'py' : lang.toLowerCase().includes('ts') ? 'ts' : 'js')
|
|
12
|
+
: (/^[ \t]*(?:class\s+\w+|(?:async\s+)?def\s+\w+)/m.test(code) ? 'py' : (/\b(?:interface\s+\w+|type\s+\w+\s*=|:\s*(?:string|number|boolean|any|void|unknown|Promise)\b)/.test(code) ? 'ts' : 'js'));
|
|
13
|
+
|
|
14
|
+
const decls = [];
|
|
15
|
+
let curr = null;
|
|
16
|
+
const isPy = detected === 'py';
|
|
17
|
+
const startRe = isPy ? /^[ \t]*(?:class\s+\w+|(?:async\s+)?def\s+\w+)/ : /^[ \t]*(?:export\b|(?:async\s+)?function\*?\b|class\b|interface\b|type\b)/;
|
|
18
|
+
const pyMatch = /^([ \t]*(?:class\s+\w+(?:\([^)]*\))?|(?:async\s+)?def\s+[\w$]+\s*\([^)]*\)\s*(?:->\s*[^:]+)?)\s*:)/;
|
|
19
|
+
const cleanJs = (s) => (/^[ \t]*export\s*\{/.test(s) ? s.trim() : s.replace(/\s*\{.*\}\s*;?$/, '').replace(/\s*\{.*$/, '').trim());
|
|
20
|
+
|
|
21
|
+
for (const line of code.split('\n')) {
|
|
22
|
+
const trimmed = line.trim();
|
|
23
|
+
if (!trimmed || (!isPy && (trimmed.startsWith('//') || trimmed.startsWith('/*')))) continue;
|
|
24
|
+
if (curr) {
|
|
25
|
+
curr += ' ' + trimmed;
|
|
26
|
+
if (isPy) {
|
|
27
|
+
const m = curr.match(pyMatch);
|
|
28
|
+
if (m) { decls.push(m[1]); curr = null; }
|
|
29
|
+
} else if (curr.includes('{') || curr.includes(';')) {
|
|
30
|
+
decls.push(cleanJs(curr));
|
|
31
|
+
curr = null;
|
|
32
|
+
}
|
|
33
|
+
} else if (startRe.test(line)) {
|
|
34
|
+
if (isPy) {
|
|
35
|
+
const m = line.match(pyMatch);
|
|
36
|
+
if (m) decls.push(m[1]);
|
|
37
|
+
else curr = line.trimEnd();
|
|
38
|
+
} else if (/^[ \t]*export\s*\{/.test(line) || /^[ \t]*type\b/.test(line)) decls.push(trimmed);
|
|
39
|
+
else if (line.includes('{')) decls.push(cleanJs(trimmed));
|
|
40
|
+
else if (line.includes(';') || (!line.includes('(') && !line.includes('function') && !line.includes('class'))) decls.push(trimmed);
|
|
41
|
+
else curr = trimmed;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { lang: detected, declarations: decls, outline: decls.join('\n') };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default sliceAst;
|
|
48
|
+
|
|
49
|
+
const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
50
|
+
if (isMain) {
|
|
51
|
+
const args = process.argv.slice(2);
|
|
52
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
53
|
+
console.log(`AST Context Slicer\n\nUsage:\n node scripts/ast_slice.js [file] [flags]\n cat file | node scripts/ast_slice.js [flags]\n\nFlags:\n --json Output JSON object instead of raw outline\n --lang=js|ts|py Force language parser\n --help, -h Show help`);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const isJson = args.includes('--json');
|
|
58
|
+
const langFlag = args.find((a) => a.startsWith('--lang='));
|
|
59
|
+
let lang = langFlag ? langFlag.split('=')[1] : 'auto';
|
|
60
|
+
const file = args.find((a) => !a.startsWith('-'));
|
|
61
|
+
if (lang === 'auto' && file) {
|
|
62
|
+
if (/\.py$/i.test(file)) lang = 'py';
|
|
63
|
+
else if (/\.tsx?$/i.test(file)) lang = 'ts';
|
|
64
|
+
else if (/\.c?jsx?$/i.test(file)) lang = 'js';
|
|
65
|
+
}
|
|
66
|
+
const raw = file ? readFileSync(file, 'utf8') : readFileSync(0, 'utf8');
|
|
67
|
+
const res = sliceAst(raw, lang);
|
|
68
|
+
console.log(isJson ? JSON.stringify(res, null, 2) : res.outline);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error(`Error: ${err.message}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Recorte de pessoa com troca de fundo (protótipo CLI-14).
|
|
3
|
+
|
|
4
|
+
Requer: rembg, onnxruntime, opencv-python-headless, numpy, Pillow, ffmpeg.
|
|
5
|
+
O CLI chama este script com --python <interpretador>.
|
|
6
|
+
"""
|
|
7
|
+
import argparse
|
|
8
|
+
import json
|
|
9
|
+
import math
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import cv2
|
|
16
|
+
import numpy as np
|
|
17
|
+
from PIL import Image
|
|
18
|
+
from rembg import new_session, remove
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def has_audio(path: str) -> bool:
|
|
22
|
+
try:
|
|
23
|
+
r = subprocess.run(
|
|
24
|
+
['ffprobe', '-v', 'error', '-select_streams', 'a',
|
|
25
|
+
'-show_entries', 'stream=codec_type', '-of', 'csv=p=0', path],
|
|
26
|
+
capture_output=True, text=True, check=False,
|
|
27
|
+
)
|
|
28
|
+
return 'audio' in r.stdout
|
|
29
|
+
except OSError:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main() -> int:
|
|
34
|
+
ap = argparse.ArgumentParser()
|
|
35
|
+
ap.add_argument('--input', required=True)
|
|
36
|
+
ap.add_argument('--background', required=True)
|
|
37
|
+
ap.add_argument('--output', required=True)
|
|
38
|
+
ap.add_argument('--fps', type=float, default=15.0)
|
|
39
|
+
ap.add_argument('--width', type=int, default=1080)
|
|
40
|
+
ap.add_argument('--height', type=int, default=1920)
|
|
41
|
+
ap.add_argument('--fg-height', type=int, default=1000)
|
|
42
|
+
ap.add_argument('--fg-y', type=int, default=None)
|
|
43
|
+
ap.add_argument('--model', default='u2net_human_seg')
|
|
44
|
+
ap.add_argument('--crf', type=int, default=20)
|
|
45
|
+
args = ap.parse_args()
|
|
46
|
+
|
|
47
|
+
if args.fps <= 0 or args.fps > 30:
|
|
48
|
+
print('fps deve estar entre 1 e 30', file=sys.stderr)
|
|
49
|
+
return 2
|
|
50
|
+
if not Path(args.input).is_file():
|
|
51
|
+
print(f'entrada não encontrada: {args.input}', file=sys.stderr)
|
|
52
|
+
return 2
|
|
53
|
+
if not Path(args.background).is_file():
|
|
54
|
+
print(f'fundo não encontrado: {args.background}', file=sys.stderr)
|
|
55
|
+
return 2
|
|
56
|
+
|
|
57
|
+
cap = cv2.VideoCapture(args.input)
|
|
58
|
+
pack = cv2.VideoCapture(args.background)
|
|
59
|
+
if not cap.isOpened() or not pack.isOpened():
|
|
60
|
+
print('não foi possível abrir entrada/fundo', file=sys.stderr)
|
|
61
|
+
return 2
|
|
62
|
+
|
|
63
|
+
src_fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
|
|
64
|
+
frames_total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
|
|
65
|
+
duration = frames_total / src_fps if frames_total else 0
|
|
66
|
+
if duration <= 0:
|
|
67
|
+
print('duração inválida na entrada', file=sys.stderr)
|
|
68
|
+
return 2
|
|
69
|
+
out_frames = max(1, math.ceil(duration * args.fps))
|
|
70
|
+
|
|
71
|
+
pack_fps = pack.get(cv2.CAP_PROP_FPS) or 30.0
|
|
72
|
+
pack_count = max(1, int(pack.get(cv2.CAP_PROP_FRAME_COUNT) or 1))
|
|
73
|
+
last_pack_idx = None
|
|
74
|
+
|
|
75
|
+
def read_bg(i):
|
|
76
|
+
nonlocal last_pack_idx
|
|
77
|
+
idx = int(round(i * pack_fps / args.fps)) % pack_count
|
|
78
|
+
if last_pack_idx is None or idx != last_pack_idx + 1:
|
|
79
|
+
pack.set(cv2.CAP_PROP_POS_FRAMES, idx)
|
|
80
|
+
ok, bg = pack.read()
|
|
81
|
+
if not ok:
|
|
82
|
+
pack.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
|
83
|
+
ok, bg = pack.read()
|
|
84
|
+
idx = 0
|
|
85
|
+
last_pack_idx = idx
|
|
86
|
+
if not ok or bg is None:
|
|
87
|
+
raise RuntimeError('falha ao ler frame do pack de fundo')
|
|
88
|
+
return cv2.resize(bg, (args.width, args.height), interpolation=cv2.INTER_AREA)
|
|
89
|
+
|
|
90
|
+
fg_h = max(1, min(args.fg_height, args.height))
|
|
91
|
+
fg_y = args.height - fg_h if args.fg_y is None else max(0, min(args.fg_y, args.height - fg_h))
|
|
92
|
+
|
|
93
|
+
audio = has_audio(args.input)
|
|
94
|
+
cmd = [
|
|
95
|
+
'ffmpeg', '-y', '-loglevel', 'error',
|
|
96
|
+
'-f', 'rawvideo', '-pix_fmt', 'bgra', '-s', f'{args.width}x{args.height}',
|
|
97
|
+
'-r', str(args.fps), '-i', '-',
|
|
98
|
+
'-i', args.input,
|
|
99
|
+
]
|
|
100
|
+
if audio:
|
|
101
|
+
maps = ['-map', '0:v', '-map', '1:a']
|
|
102
|
+
else:
|
|
103
|
+
cmd += ['-f', 'lavfi', '-i', 'anullsrc=channel_layout=stereo:sample_rate=44100']
|
|
104
|
+
maps = ['-map', '0:v', '-map', '2:a']
|
|
105
|
+
cmd += maps + [
|
|
106
|
+
'-c:v', 'libx264', '-preset', 'veryfast', '-crf', str(args.crf),
|
|
107
|
+
'-pix_fmt', 'yuv420p', '-c:a', 'aac', '-b:a', '128k',
|
|
108
|
+
'-shortest', '-movflags', '+faststart', args.output,
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
print(f'Carregando modelo {args.model} (primeira execução pode baixar ~176 MB)...', file=sys.stderr, flush=True)
|
|
112
|
+
session = new_session(args.model)
|
|
113
|
+
ff = subprocess.Popen(cmd, stdin=subprocess.PIPE)
|
|
114
|
+
started = time.time()
|
|
115
|
+
written = 0
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
for i in range(out_frames):
|
|
119
|
+
cap.set(cv2.CAP_PROP_POS_MSEC, i * 1000.0 / args.fps)
|
|
120
|
+
ok, frame = cap.read()
|
|
121
|
+
if not ok:
|
|
122
|
+
break
|
|
123
|
+
bg = read_bg(i)
|
|
124
|
+
cut = remove(
|
|
125
|
+
Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)),
|
|
126
|
+
session=session,
|
|
127
|
+
post_process_mask=True,
|
|
128
|
+
)
|
|
129
|
+
# rembg devolve RGBA em RGB; OpenCV compõe em BGR. Sem esta conversão,
|
|
130
|
+
# vermelho e azul trocam e a pele fica azul.
|
|
131
|
+
fg = cv2.cvtColor(np.array(cut), cv2.COLOR_RGBA2BGRA)
|
|
132
|
+
# Refina a máscara com GrabCut numa faixa ao redor do contorno.
|
|
133
|
+
# O u2net devolve uma silhueta frouxa (15-50 px de fundo ao redor
|
|
134
|
+
# da cabeça); sem refino, esse fundo vira um halo escuro no composto.
|
|
135
|
+
mask = (fg[:, :, 3] > 127).astype(np.uint8) * 255
|
|
136
|
+
try:
|
|
137
|
+
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))
|
|
138
|
+
core = cv2.erode(mask, kernel)
|
|
139
|
+
outside = cv2.bitwise_not(cv2.dilate(mask, kernel))
|
|
140
|
+
gc = np.full(mask.shape, cv2.GC_PR_BGD, np.uint8)
|
|
141
|
+
gc[outside > 127] = cv2.GC_BGD
|
|
142
|
+
gc[mask > 127] = cv2.GC_PR_FGD
|
|
143
|
+
gc[core > 127] = cv2.GC_FGD
|
|
144
|
+
bgm = np.zeros((1, 65), np.float64)
|
|
145
|
+
fgm = np.zeros((1, 65), np.float64)
|
|
146
|
+
cv2.grabCut(frame, gc, None, bgm, fgm, 1, cv2.GC_INIT_WITH_MASK)
|
|
147
|
+
refined = np.where((gc == cv2.GC_FGD) | (gc == cv2.GC_PR_FGD), 255, 0).astype(np.uint8)
|
|
148
|
+
if refined.any():
|
|
149
|
+
mask = refined
|
|
150
|
+
except cv2.error:
|
|
151
|
+
pass
|
|
152
|
+
a = mask.astype(np.float32) / 255.0
|
|
153
|
+
fg[:, :, 3] = (cv2.GaussianBlur(a, (3, 3), 0) * 255.0).astype(np.uint8)
|
|
154
|
+
nw = max(1, int(fg.shape[1] * fg_h / fg.shape[0]))
|
|
155
|
+
fg = cv2.resize(fg, (nw, fg_h), interpolation=cv2.INTER_AREA)
|
|
156
|
+
if nw >= args.width:
|
|
157
|
+
x0 = (nw - args.width) // 2
|
|
158
|
+
fg = fg[:, x0:x0 + args.width]
|
|
159
|
+
else:
|
|
160
|
+
pad = np.zeros((fg_h, args.width, 4), dtype=np.uint8)
|
|
161
|
+
x0 = (args.width - nw) // 2
|
|
162
|
+
pad[:, x0:x0 + nw] = fg
|
|
163
|
+
fg = pad
|
|
164
|
+
|
|
165
|
+
alpha = (fg[:, :, 3:4].astype(np.float32) / 255.0)
|
|
166
|
+
roi = bg[fg_y:fg_y + fg_h, 0:args.width].astype(np.float32)
|
|
167
|
+
comp = fg[:, :, :3].astype(np.float32) * alpha + roi * (1.0 - alpha)
|
|
168
|
+
bg[fg_y:fg_y + fg_h, 0:args.width] = np.clip(comp, 0, 255).astype(np.uint8)
|
|
169
|
+
bgra = cv2.cvtColor(bg, cv2.COLOR_BGR2BGRA)
|
|
170
|
+
ff.stdin.write(bgra.tobytes())
|
|
171
|
+
written += 1
|
|
172
|
+
if written % 100 == 0:
|
|
173
|
+
print(f'{written}/{out_frames} frames — {time.time() - started:.1f}s', file=sys.stderr, flush=True)
|
|
174
|
+
ff.stdin.close()
|
|
175
|
+
rc = ff.wait()
|
|
176
|
+
if rc != 0:
|
|
177
|
+
print(f'ffmpeg falhou (exit {rc})', file=sys.stderr)
|
|
178
|
+
return rc
|
|
179
|
+
except BrokenPipeError:
|
|
180
|
+
print('ffmpeg encerrou antes do fim dos frames', file=sys.stderr)
|
|
181
|
+
return 1
|
|
182
|
+
finally:
|
|
183
|
+
cap.release()
|
|
184
|
+
pack.release()
|
|
185
|
+
|
|
186
|
+
print(json.dumps({
|
|
187
|
+
'status': 'ok',
|
|
188
|
+
'frames': written,
|
|
189
|
+
'elapsed_s': round(time.time() - started, 1),
|
|
190
|
+
'has_audio': audio,
|
|
191
|
+
'output': args.output,
|
|
192
|
+
}), flush=True)
|
|
193
|
+
return 0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if __name__ == '__main__':
|
|
197
|
+
sys.exit(main())
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
|
|
7
|
+
// ponytail: in-memory Kahn algorithm; upgrade to streaming DAG for 100k+ nodes
|
|
8
|
+
export function validateDAG(tasks = []) {
|
|
9
|
+
if (!Array.isArray(tasks)) return { valid: false, cycles: [], missing_dependencies: [], waves: [] };
|
|
10
|
+
|
|
11
|
+
const inDegree = new Map();
|
|
12
|
+
const dependents = new Map();
|
|
13
|
+
const knownIds = new Set(tasks.map((t) => t.id)), missing_dependencies = [];
|
|
14
|
+
|
|
15
|
+
for (const t of tasks) {
|
|
16
|
+
inDegree.set(t.id, 0);
|
|
17
|
+
dependents.set(t.id, []);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (const t of tasks) {
|
|
21
|
+
for (const dep of new Set(t.depends_on || [])) {
|
|
22
|
+
if (!knownIds.has(dep)) { if (!missing_dependencies.includes(dep)) missing_dependencies.push(dep); }
|
|
23
|
+
else { inDegree.set(t.id, inDegree.get(t.id) + 1); dependents.get(dep).push(t.id); }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const waves = [];
|
|
28
|
+
let currentWave = [];
|
|
29
|
+
for (const t of tasks) {
|
|
30
|
+
if (inDegree.get(t.id) === 0) currentWave.push(t.id);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let processed = 0;
|
|
34
|
+
while (currentWave.length > 0) {
|
|
35
|
+
waves.push(currentWave);
|
|
36
|
+
processed += currentWave.length;
|
|
37
|
+
const nextWave = [];
|
|
38
|
+
for (const id of currentWave) {
|
|
39
|
+
for (const nextId of dependents.get(id) || []) {
|
|
40
|
+
const deg = inDegree.get(nextId) - 1;
|
|
41
|
+
inDegree.set(nextId, deg);
|
|
42
|
+
if (deg === 0) nextWave.push(nextId);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
currentWave = nextWave;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const valid = processed === tasks.length && missing_dependencies.length === 0;
|
|
49
|
+
const cycles = valid ? [] : tasks.filter((t) => (inDegree.get(t.id) || 0) > 0).map((t) => t.id);
|
|
50
|
+
|
|
51
|
+
return { valid, cycles, missing_dependencies, waves };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default validateDAG;
|
|
55
|
+
|
|
56
|
+
const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
57
|
+
if (isMain) {
|
|
58
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
59
|
+
console.log(`DAG Validator
|
|
60
|
+
|
|
61
|
+
Usage:
|
|
62
|
+
node scripts/dag_validator.js [tasks.json]
|
|
63
|
+
cat tasks.json | node scripts/dag_validator.js
|
|
64
|
+
|
|
65
|
+
Validates DAG acyclicity and computes topological execution waves.`);
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const raw = process.argv[2] ? readFileSync(process.argv[2], 'utf8') : readFileSync(0, 'utf8');
|
|
71
|
+
const result = validateDAG(JSON.parse(raw));
|
|
72
|
+
console.log(JSON.stringify(result, null, 2));
|
|
73
|
+
if (!result.valid) process.exitCode = 1;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error(`Error: ${err.message}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
package/scripts/lint.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const repoRoot = fileURLToPath(new URL('..', import.meta.url));
|
|
9
|
+
const targetDirs = ['bin', 'src', 'scripts', 'tests'];
|
|
10
|
+
|
|
11
|
+
function collectJsFiles(dir) {
|
|
12
|
+
const fullPath = path.join(repoRoot, dir);
|
|
13
|
+
if (!fs.existsSync(fullPath)) return [];
|
|
14
|
+
const entries = fs.readdirSync(fullPath, { withFileTypes: true });
|
|
15
|
+
const files = [];
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
const entryPath = path.join(fullPath, entry.name);
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
files.push(...collectJsFiles(path.relative(repoRoot, entryPath)));
|
|
20
|
+
} else if (entry.isFile() && entry.name.endsWith('.js')) {
|
|
21
|
+
files.push(entryPath);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return files;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const allJsFiles = targetDirs.flatMap(collectJsFiles);
|
|
28
|
+
|
|
29
|
+
let failed = 0;
|
|
30
|
+
for (const file of allJsFiles) {
|
|
31
|
+
try {
|
|
32
|
+
execFileSync(process.execPath, ['--check', file], { stdio: 'inherit' });
|
|
33
|
+
} catch {
|
|
34
|
+
failed++;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (failed > 0) {
|
|
39
|
+
console.error(`Lint check failed: ${failed} file(s) with syntax errors.`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(`Lint passed: verified ${allJsFiles.length} files with node --check.`);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
|
|
8
|
+
// ponytail: in-memory linear ledger verifier; upgrade to streaming Merkle tree for 10M+ blocks
|
|
9
|
+
export function canonicalStringify(val) {
|
|
10
|
+
if (val === null || typeof val !== 'object') return JSON.stringify(val);
|
|
11
|
+
if (Array.isArray(val)) return `[${val.map((v) => (v === undefined ? 'null' : canonicalStringify(v))).join(',')}]`;
|
|
12
|
+
const keys = Object.keys(val).filter((k) => val[k] !== undefined).sort();
|
|
13
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalStringify(val[k])}`).join(',')}}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function computeBlockHash(block) {
|
|
17
|
+
if (!block || typeof block !== 'object') return '';
|
|
18
|
+
const { hash: _, ...payload } = block;
|
|
19
|
+
return createHash('sha256').update(canonicalStringify(payload)).digest('hex');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function verifyLedger(blocks) {
|
|
23
|
+
const list = Array.isArray(blocks) ? blocks : (Array.isArray(blocks?.blocks) ? blocks.blocks : null);
|
|
24
|
+
if (!list) return { valid: false, blocks_verified: 0, errors: ['Ledger must be an array of blocks'] };
|
|
25
|
+
if (list.length === 0) return { valid: true, blocks_verified: 0, errors: [] };
|
|
26
|
+
|
|
27
|
+
const errors = [];
|
|
28
|
+
const isGenesisPrevValid = (prev) =>
|
|
29
|
+
prev === null || prev === undefined || prev === 0 || prev === '0' || prev === '' ||
|
|
30
|
+
(typeof prev === 'string' && /^[0-9a-fA-F]{64}$/.test(prev));
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < list.length; i++) {
|
|
33
|
+
const block = list[i];
|
|
34
|
+
if (!block || typeof block !== 'object') {
|
|
35
|
+
errors.push(`Block ${i}: block must be an object`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (i === 0) {
|
|
40
|
+
if (!isGenesisPrevValid(block.previous_hash)) {
|
|
41
|
+
errors.push(`Block 0: invalid previous_hash (${block.previous_hash})`);
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
const prevHash = list[i - 1]?.hash;
|
|
45
|
+
if (block.previous_hash !== prevHash) {
|
|
46
|
+
errors.push(`Block ${i}: previous_hash mismatch (expected ${prevHash}, got ${block.previous_hash})`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const computed = computeBlockHash(block);
|
|
51
|
+
if (!block.hash || block.hash !== computed) {
|
|
52
|
+
errors.push(`Block ${i}: hash mismatch (declared ${block.hash}, computed ${computed})`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { valid: errors.length === 0, blocks_verified: list.length, errors };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default verifyLedger;
|
|
60
|
+
|
|
61
|
+
const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
62
|
+
if (isMain) {
|
|
63
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
64
|
+
console.log(`Merkle Ledger Integrity Verifier
|
|
65
|
+
|
|
66
|
+
Usage:
|
|
67
|
+
node scripts/merkle_verify.js [ledger.json]
|
|
68
|
+
cat ledger.json | node scripts/merkle_verify.js
|
|
69
|
+
|
|
70
|
+
Validates cryptographic hash continuity and payload integrity across blocks.`);
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const raw = process.argv[2] ? readFileSync(process.argv[2], 'utf8') : readFileSync(0, 'utf8');
|
|
76
|
+
const result = verifyLedger(JSON.parse(raw));
|
|
77
|
+
console.log(JSON.stringify(result, null, 2));
|
|
78
|
+
if (!result.valid) process.exitCode = 1;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error(`Error: ${err.message}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
|
|
7
|
+
// ponytail: sRGB/WCAG 2.1 relative luminance; upgrade to APCA/WCAG 3 for perceptual lightness
|
|
8
|
+
export function parseColor(str) {
|
|
9
|
+
if (str && typeof str === 'object' && typeof str.r === 'number') return str;
|
|
10
|
+
if (typeof str !== 'string') return null;
|
|
11
|
+
let h = str.trim().replace(/^#/, '');
|
|
12
|
+
if (h.length === 3) h = h.split('').map((c) => c + c).join('');
|
|
13
|
+
if (h.length !== 6 || !/^[0-9a-fA-F]{6}$/.test(h)) return null;
|
|
14
|
+
const n = parseInt(h, 16);
|
|
15
|
+
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getLuminance(color) {
|
|
19
|
+
const rgb = parseColor(color);
|
|
20
|
+
if (!rgb) return 0;
|
|
21
|
+
const srgb = [rgb.r, rgb.g, rgb.b].map((v) => {
|
|
22
|
+
const c = v / 255;
|
|
23
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
24
|
+
});
|
|
25
|
+
return 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getContrastRatio(fg, bg) {
|
|
29
|
+
const l1 = getLuminance(fg), l2 = getLuminance(bg);
|
|
30
|
+
return Math.round(((Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05)) * 100) / 100;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function checkWcag(params = {}) {
|
|
34
|
+
const fg = params.fg ?? params.foreground, bg = params.bg ?? params.background;
|
|
35
|
+
const validColors = Boolean(parseColor(fg) && parseColor(bg));
|
|
36
|
+
const ratio = validColors ? getContrastRatio(fg, bg) : 1;
|
|
37
|
+
const passAA = validColors && ratio >= 4.5, passLarge = validColors && ratio >= 3.0;
|
|
38
|
+
const width = Number(params.width ?? 44), height = Number(params.height ?? 44);
|
|
39
|
+
const targetPass = width >= 44 && height >= 44;
|
|
40
|
+
return {
|
|
41
|
+
contrast: { ratio, passAA, passLarge },
|
|
42
|
+
targetSize: { pass: targetPass, width, height },
|
|
43
|
+
valid: Boolean(passAA && targetPass),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default checkWcag;
|
|
48
|
+
|
|
49
|
+
const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
50
|
+
if (isMain) {
|
|
51
|
+
const args = process.argv.slice(2);
|
|
52
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
53
|
+
console.log(`WCAG 2.1 Checker
|
|
54
|
+
|
|
55
|
+
Usage: node scripts/wcag_check.js --fg="#000" --bg="#fff" [--width=44] [--height=44] [--json]
|
|
56
|
+
Flags: --fg=<hex>, --bg=<hex>, --width=<px>, --height=<px>, --json, --help`);
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
const getArg = (k) => {
|
|
60
|
+
const eq = args.find((x) => x.startsWith(k + '='));
|
|
61
|
+
if (eq) return eq.slice(k.length + 1).replace(/^["']|["']$/g, '');
|
|
62
|
+
const idx = args.indexOf(k);
|
|
63
|
+
return idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith('-') ? args[idx + 1].replace(/^["']|["']$/g, '') : undefined;
|
|
64
|
+
};
|
|
65
|
+
const params = {
|
|
66
|
+
fg: getArg('--fg') ?? '#000000', bg: getArg('--bg') ?? '#ffffff',
|
|
67
|
+
width: getArg('--width') ? Number(getArg('--width')) : 44, height: getArg('--height') ? Number(getArg('--height')) : 44,
|
|
68
|
+
};
|
|
69
|
+
const file = args.find((a) => !a.startsWith('-'));
|
|
70
|
+
if (file) { try { Object.assign(params, JSON.parse(readFileSync(file, 'utf8'))); } catch {} }
|
|
71
|
+
const res = checkWcag(params);
|
|
72
|
+
if (args.includes('--json')) console.log(JSON.stringify(res, null, 2));
|
|
73
|
+
else console.log(`WCAG 2.1 Audit: ${res.valid ? 'PASSED' : 'FAILED'}
|
|
74
|
+
Contrast: ${res.contrast.ratio}:1 (AA: ${res.contrast.passAA ? 'PASS' : 'FAIL'}, Large: ${res.contrast.passLarge ? 'PASS' : 'FAIL'})
|
|
75
|
+
Target Size: ${res.targetSize.width}x${res.targetSize.height}px (${res.targetSize.pass ? 'PASS' : 'FAIL'})`);
|
|
76
|
+
if (!res.valid) process.exitCode = 1;
|
|
77
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skill-audit
|
|
3
|
+
version: "1.0.0"
|
|
4
|
+
description: "Verify cryptographic Merkle ledgers, validate execution receipts, and enable time-travel replays."
|
|
5
|
+
allowed-tools: ["terminal"]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Agent Audit
|
|
9
|
+
|
|
10
|
+
Validate tamper-evident execution ledgers, check cryptographic receipts, and audit historical agent runs.
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
- Verifying SHA-256 Merkle chain integrity across task executions.
|
|
15
|
+
- Auditing cryptographic receipts and provenance of generated artifacts.
|
|
16
|
+
- Deterministic time-travel replay of prior mission checkpoints.
|
|
17
|
+
|
|
18
|
+
## NOT for
|
|
19
|
+
|
|
20
|
+
- Real-time swarm leasing (`skill-orchestrate`).
|
|
21
|
+
- WCAG color contrast checks (`skill-ui`).
|
|
22
|
+
- In-memory AST slicing (`skill-context`).
|
|
23
|
+
|
|
24
|
+
## Workflow & CLI
|
|
25
|
+
|
|
26
|
+
Execute via local script:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Verify ledger continuity and payload hashes
|
|
30
|
+
node scripts/merkle_verify.js ledger.json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## UX & Operational Rules
|
|
34
|
+
|
|
35
|
+
- Ledgers must maintain strict `previous_hash` continuity across all blocks.
|
|
36
|
+
- Genesis blocks must declare valid genesis previous hashes (`0` or null).
|
|
37
|
+
|
|
38
|
+
## References
|
|
39
|
+
|
|
40
|
+
- Merkle ledger specification: [references/merkle-ledger-spec.md](references/merkle-ledger-spec.md)
|
|
41
|
+
- Receipt verification: [references/receipt-ed25519-verification.md](references/receipt-ed25519-verification.md)
|
|
42
|
+
- Time-travel replay: [references/time-travel-replay.md](references/time-travel-replay.md)
|