@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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +366 -2
  3. package/assets/fonts/Anton-Regular.ttf +0 -0
  4. package/assets/fonts/BebasNeue-Regular.ttf +0 -0
  5. package/assets/fonts/OFL.txt +93 -0
  6. package/assets/fonts/Poppins-Bold.ttf +0 -0
  7. package/assets/fonts/README.md +13 -0
  8. package/bin/cli.js +126 -0
  9. package/package.json +58 -4
  10. package/scripts/ast_slice.js +73 -0
  11. package/scripts/cutout.py +197 -0
  12. package/scripts/dag_validator.js +78 -0
  13. package/scripts/lint.js +43 -0
  14. package/scripts/merkle_verify.js +83 -0
  15. package/scripts/wcag_check.js +77 -0
  16. package/skill-audit/SKILL.md +42 -0
  17. package/skill-audit/references/merkle-ledger-spec.md +27 -0
  18. package/skill-audit/references/receipt-ed25519-verification.md +17 -0
  19. package/skill-audit/references/time-travel-replay.md +10 -0
  20. package/skill-code-reviewer/SKILL.md +40 -0
  21. package/skill-context/SKILL.md +44 -0
  22. package/skill-context/references/ast-virtualization.md +10 -0
  23. package/skill-context/references/openapi-contract-synthesis.md +10 -0
  24. package/skill-engineering-manager/SKILL.md +40 -0
  25. package/skill-media-producer/SKILL.md +328 -0
  26. package/skill-media-producer/references/caption-templates.md +194 -0
  27. package/skill-media-producer/references/composicoes-especiais.md +99 -0
  28. package/skill-media-producer/references/configuracao-inicial.md +239 -0
  29. package/skill-media-producer/references/corte-podcast-3-niveis.md +179 -0
  30. package/skill-media-producer/references/llm-agent-guide.md +231 -0
  31. package/skill-orchestrate/SKILL.md +49 -0
  32. package/skill-orchestrate/references/mission-lifecycle.md +36 -0
  33. package/skill-orchestrate/references/swarm-coordination.md +33 -0
  34. package/skill-pipeline/SKILL.md +42 -0
  35. package/skill-pipeline/references/auto-healing-patterns.md +17 -0
  36. package/skill-pipeline/references/dag-decomposition-rules.md +18 -0
  37. package/skill-pipeline/references/topological-waves.md +16 -0
  38. package/skill-product-manager/SKILL.md +40 -0
  39. package/skill-qa-engineer/SKILL.md +40 -0
  40. package/skill-review/SKILL.md +46 -0
  41. package/skill-review/references/anti-slop-patterns.md +17 -0
  42. package/skill-review/references/dual-blind-adjudication.md +15 -0
  43. package/skill-review/references/voice-calibration.md +10 -0
  44. package/skill-security-engineer/SKILL.md +40 -0
  45. package/skill-software-architect/SKILL.md +40 -0
  46. package/skill-software-engineer/SKILL.md +40 -0
  47. package/skill-sre-engineer/SKILL.md +40 -0
  48. package/skill-system-analyst/SKILL.md +40 -0
  49. package/skill-tech-lead/SKILL.md +40 -0
  50. package/skill-technical-writer/SKILL.md +40 -0
  51. package/skill-ui/SKILL.md +45 -0
  52. package/skill-ui/references/design-tokens-spec.md +19 -0
  53. package/skill-ui/references/visual-tdd-methodology.md +10 -0
  54. package/skill-ui/references/wcag-target-sizes.md +9 -0
  55. package/src/captionTemplates.js +285 -0
  56. package/src/commands/onboard.js +618 -0
  57. package/src/commands/role.js +119 -0
  58. package/src/commands/setup.js +174 -0
  59. package/src/commands/studio.js +1822 -0
  60. package/src/roles/authority.js +129 -0
  61. package/src/roles/codeReviewer.js +65 -0
  62. package/src/roles/engineeringManager.js +53 -0
  63. package/src/roles/index.js +80 -0
  64. package/src/roles/mediaProducer.js +223 -0
  65. package/src/roles/productManager.js +50 -0
  66. package/src/roles/qaEngineer.js +54 -0
  67. package/src/roles/securityEngineer.js +53 -0
  68. package/src/roles/softwareArchitect.js +58 -0
  69. package/src/roles/softwareEngineer.js +76 -0
  70. package/src/roles/sreEngineer.js +60 -0
  71. package/src/roles/systemAnalyst.js +45 -0
  72. package/src/roles/techLead.js +46 -0
  73. package/src/roles/technicalWriter.js +46 -0
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "@esquadrao/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }
@@ -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)