@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
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Structural Authority Matrix and Permission Guard for Engineering Roles.
|
|
2
|
+
// Zero-dependency native Node.js implementation.
|
|
3
|
+
|
|
4
|
+
import { definition as productManager } from './productManager.js';
|
|
5
|
+
import { definition as systemAnalyst } from './systemAnalyst.js';
|
|
6
|
+
import { definition as softwareArchitect } from './softwareArchitect.js';
|
|
7
|
+
import { definition as techLead } from './techLead.js';
|
|
8
|
+
import { definition as securityEngineer } from './securityEngineer.js';
|
|
9
|
+
import { definition as softwareEngineer } from './softwareEngineer.js';
|
|
10
|
+
import { definition as codeReviewer } from './codeReviewer.js';
|
|
11
|
+
import { definition as sreEngineer } from './sreEngineer.js';
|
|
12
|
+
import { definition as qaEngineer } from './qaEngineer.js';
|
|
13
|
+
import { definition as technicalWriter } from './technicalWriter.js';
|
|
14
|
+
import { definition as engineeringManager } from './engineeringManager.js';
|
|
15
|
+
import { definition as mediaProducer } from './mediaProducer.js';
|
|
16
|
+
|
|
17
|
+
const ROLE_DEFINITIONS = {
|
|
18
|
+
'product-manager': productManager,
|
|
19
|
+
'system-analyst': systemAnalyst,
|
|
20
|
+
'software-architect': softwareArchitect,
|
|
21
|
+
'tech-lead': techLead,
|
|
22
|
+
'security-engineer': securityEngineer,
|
|
23
|
+
'software-engineer': softwareEngineer,
|
|
24
|
+
'code-reviewer': codeReviewer,
|
|
25
|
+
'sre-engineer': sreEngineer,
|
|
26
|
+
'qa-engineer': qaEngineer,
|
|
27
|
+
'technical-writer': technicalWriter,
|
|
28
|
+
'engineering-manager': engineeringManager,
|
|
29
|
+
'media-producer': mediaProducer,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const LEGACY_ALIASES = {
|
|
33
|
+
mission_compiler: 'product-manager',
|
|
34
|
+
cartographer: 'system-analyst',
|
|
35
|
+
chief_architect: 'software-architect',
|
|
36
|
+
architecture_adjudicator: 'tech-lead',
|
|
37
|
+
spec_critic: 'security-engineer',
|
|
38
|
+
implementer: 'software-engineer',
|
|
39
|
+
reviewer: 'code-reviewer',
|
|
40
|
+
failure_diagnostician: 'sre-engineer',
|
|
41
|
+
critical_acceptance: 'qa-engineer',
|
|
42
|
+
documentation: 'technical-writer',
|
|
43
|
+
next_mission_generator: 'engineering-manager',
|
|
44
|
+
studio_director: 'media-producer',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const FORBIDDEN_COMMAND_PATTERNS = [
|
|
48
|
+
/git\s+push\b.*?\b(main|master)\b/i,
|
|
49
|
+
/git\s+push\b.*?\s+(-f|--force)\b/i,
|
|
50
|
+
/git\s+merge\b.*?\b(main|master)\b/i,
|
|
51
|
+
/git\s+reset\s+--hard\b/i,
|
|
52
|
+
/rm\s+-rf\s+[\/\\]/i,
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export const READ_ONLY_ROLES = new Set([
|
|
56
|
+
'system-analyst',
|
|
57
|
+
'security-engineer',
|
|
58
|
+
'code-reviewer',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
export const ALLOWED_READ_COMMAND_PREFIXES = [
|
|
62
|
+
'git status',
|
|
63
|
+
'git diff',
|
|
64
|
+
'git log',
|
|
65
|
+
'git rev-parse',
|
|
66
|
+
'git branch',
|
|
67
|
+
'node -v',
|
|
68
|
+
'npm test',
|
|
69
|
+
'dir',
|
|
70
|
+
'ls',
|
|
71
|
+
'find',
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
export function checkCommandExecution(roleId, command) {
|
|
75
|
+
const cmd = (command || '').trim();
|
|
76
|
+
for (const pattern of FORBIDDEN_COMMAND_PATTERNS) {
|
|
77
|
+
if (pattern.test(cmd)) {
|
|
78
|
+
throw new Error(`Structural violation: Command matches forbidden pattern '${pattern}' for role '${roleId}'.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const normalized = (roleId || '').toLowerCase().replace(/_/g, '-');
|
|
83
|
+
const canonical = LEGACY_ALIASES[roleId?.toLowerCase()] || normalized;
|
|
84
|
+
if (READ_ONLY_ROLES.has(roleId) || READ_ONLY_ROLES.has(canonical)) {
|
|
85
|
+
const isAllowed = ALLOWED_READ_COMMAND_PREFIXES.some((prefix) =>
|
|
86
|
+
cmd.toLowerCase().startsWith(prefix.toLowerCase())
|
|
87
|
+
);
|
|
88
|
+
if (!isAllowed) {
|
|
89
|
+
throw new Error(`Structural violation: Role '${roleId}' is strictly read-only and cannot execute mutating command: '${cmd}'.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function checkOutputWrite(roleId, filePath) {
|
|
95
|
+
const normalized = (roleId || '').toLowerCase().replace(/_/g, '-');
|
|
96
|
+
const canonical = LEGACY_ALIASES[roleId?.toLowerCase()] || normalized;
|
|
97
|
+
const def = ROLE_DEFINITIONS[canonical];
|
|
98
|
+
|
|
99
|
+
if (!def) {
|
|
100
|
+
throw new Error(`Structural violation: Unknown role '${roleId}'.`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const prefixes = def.allowedOutputPrefixes || def.allowedOutputs || [];
|
|
104
|
+
if (prefixes.includes('*')) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ponytail: relative prefix and exact match only; upgrade to glob engine if complex wildcard patterns needed
|
|
109
|
+
const normalizedPath = filePath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
110
|
+
const isAllowed = prefixes.some((prefix) => {
|
|
111
|
+
if (prefix.endsWith('/')) {
|
|
112
|
+
return normalizedPath.startsWith(prefix) || normalizedPath === prefix.slice(0, -1);
|
|
113
|
+
}
|
|
114
|
+
return normalizedPath === prefix;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!isAllowed) {
|
|
118
|
+
throw new Error(`Structural violation: File '${filePath}' violates allowed output prefixes for role '${roleId}'. Allowed: ${prefixes.join(', ')}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function checkFileWrite(roleId, filePath) {
|
|
123
|
+
const normalized = (roleId || '').toLowerCase().replace(/_/g, '-');
|
|
124
|
+
const canonical = LEGACY_ALIASES[roleId?.toLowerCase()] || normalized;
|
|
125
|
+
if (READ_ONLY_ROLES.has(roleId) || READ_ONLY_ROLES.has(canonical)) {
|
|
126
|
+
throw new Error(`Structural violation: Role '${roleId}' is strictly read-only. File write denied for '${filePath}'.`);
|
|
127
|
+
}
|
|
128
|
+
checkOutputWrite(roleId, filePath);
|
|
129
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Code Reviewer (formerly reviewer)
|
|
2
|
+
// Independent verification, anti-slop pattern linting, code quality inspection, and anti-self-review enforcement.
|
|
3
|
+
|
|
4
|
+
export const definition = {
|
|
5
|
+
id: 'code-reviewer',
|
|
6
|
+
title: 'Code Reviewer',
|
|
7
|
+
formerName: 'reviewer',
|
|
8
|
+
category: 'quality',
|
|
9
|
+
description: 'Conducts independent peer code reviews, filters out boilerplate/slop, validates diff correctness, and enforces anti-self-review.',
|
|
10
|
+
requiredCapabilities: ['chat'],
|
|
11
|
+
readOnly: true,
|
|
12
|
+
allowedOutputPrefixes: ['reports/'],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function buildPrompt(params = {}) {
|
|
16
|
+
const { diff = '', objective = '', implementerId = '', reviewerId = '' } = params;
|
|
17
|
+
|
|
18
|
+
if (implementerId && reviewerId && implementerId === reviewerId) {
|
|
19
|
+
throw new Error(`Anti-self-review violation: Reviewer '${reviewerId}' cannot review its own work from '${implementerId}'.`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return `You are acting as an Independent Code Reviewer.
|
|
23
|
+
Inspect the provided diff with adversarial scrutiny.
|
|
24
|
+
Verify that the diff is minimal, adheres strictly to requirements, avoids speculative abstractions, and includes tests.
|
|
25
|
+
|
|
26
|
+
Objective:
|
|
27
|
+
${objective}
|
|
28
|
+
|
|
29
|
+
Git Diff:
|
|
30
|
+
\`\`\`diff
|
|
31
|
+
${diff}
|
|
32
|
+
\`\`\`
|
|
33
|
+
|
|
34
|
+
Anti-Slop Checklist:
|
|
35
|
+
- No unused interfaces or single-implementation factories.
|
|
36
|
+
- No empty catch blocks or suppressed errors.
|
|
37
|
+
- No unrequested dependencies.
|
|
38
|
+
- No decorative/tautological comments.
|
|
39
|
+
- Test coverage present and passing.
|
|
40
|
+
|
|
41
|
+
Output Requirements:
|
|
42
|
+
Return a structured JSON object with:
|
|
43
|
+
{
|
|
44
|
+
"verdict": "PASS | FAIL_MINOR | FAIL_MAJOR | BLOCKER",
|
|
45
|
+
"findings": [
|
|
46
|
+
{
|
|
47
|
+
"severity": "INFO | MINOR | MAJOR | BLOCKER",
|
|
48
|
+
"file": "path/to/file.ext",
|
|
49
|
+
"line": 42,
|
|
50
|
+
"issue": "Specific description of bug or violation",
|
|
51
|
+
"suggestion": "How to fix"
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"recommendation": "Ready to merge | Requires targeted fixes | Reject"
|
|
55
|
+
}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function validateContract(data) {
|
|
59
|
+
if (!data || typeof data !== 'object') throw new Error('Contract must be an object');
|
|
60
|
+
if (!['PASS', 'FAIL_MINOR', 'FAIL_MAJOR', 'BLOCKER'].includes(data.verdict)) {
|
|
61
|
+
throw new Error('Invalid review verdict');
|
|
62
|
+
}
|
|
63
|
+
if (!Array.isArray(data.findings)) throw new Error('"findings" must be an array');
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Engineering Delivery Manager / Agile Coach (formerly next_mission_generator)
|
|
2
|
+
// Derives follow-up engineering milestones, roadmap planning, and task backlogs from actual execution evidence.
|
|
3
|
+
|
|
4
|
+
export const definition = {
|
|
5
|
+
id: 'engineering-manager',
|
|
6
|
+
title: 'Engineering Manager',
|
|
7
|
+
formerName: 'next_mission_generator',
|
|
8
|
+
category: 'management',
|
|
9
|
+
description: 'Synthesizes completed project evidence into follow-up milestones, roadmap iterations, and next sprint tasks.',
|
|
10
|
+
requiredCapabilities: ['chat'],
|
|
11
|
+
readOnly: false,
|
|
12
|
+
allowedOutputPrefixes: ['docs/', 'reports/', 'NEXT_MISSION.md'],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function buildPrompt(params = {}) {
|
|
16
|
+
const { completedTask = '', verdict = '', remainingDebt = [] } = params;
|
|
17
|
+
return `You are acting as an Engineering Delivery Manager and Agile Lead.
|
|
18
|
+
Derive the next actionable milestone and sprint backlog based on verified execution evidence.
|
|
19
|
+
|
|
20
|
+
Completed Task:
|
|
21
|
+
${completedTask}
|
|
22
|
+
|
|
23
|
+
Acceptance Verdict:
|
|
24
|
+
${verdict}
|
|
25
|
+
|
|
26
|
+
Remaining Technical Debt / Future Scope:
|
|
27
|
+
${remainingDebt.map((d) => `- ${d}`).join('\n') || '- None noted'}
|
|
28
|
+
|
|
29
|
+
Output Requirements:
|
|
30
|
+
Return a structured JSON object with:
|
|
31
|
+
{
|
|
32
|
+
"next_milestone": "Concise title of the subsequent milestone",
|
|
33
|
+
"priority": "P0 | P1 | P2",
|
|
34
|
+
"objectives": ["concrete objective 1", "concrete objective 2"],
|
|
35
|
+
"tasks": [
|
|
36
|
+
{
|
|
37
|
+
"id": "TASK-01",
|
|
38
|
+
"title": "Task title",
|
|
39
|
+
"assigned_role": "role identifier",
|
|
40
|
+
"description": "Short description of expected output"
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"invariants_to_preserve": ["system guarantees that remain protected"]
|
|
44
|
+
}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function validateContract(data) {
|
|
48
|
+
if (!data || typeof data !== 'object') throw new Error('Contract must be an object');
|
|
49
|
+
if (!data.next_milestone || typeof data.next_milestone !== 'string') throw new Error('Missing "next_milestone"');
|
|
50
|
+
if (!Array.isArray(data.objectives)) throw new Error('"objectives" must be an array');
|
|
51
|
+
if (!Array.isArray(data.tasks) || data.tasks.length === 0) throw new Error('"tasks" must be a non-empty array');
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Canonical Registry of Industry Roles for AI Agent Orchestration.
|
|
2
|
+
// Zero dependencies - Node.js stdlib only.
|
|
3
|
+
|
|
4
|
+
import * as productManager from './productManager.js';
|
|
5
|
+
import * as systemAnalyst from './systemAnalyst.js';
|
|
6
|
+
import * as softwareArchitect from './softwareArchitect.js';
|
|
7
|
+
import * as techLead from './techLead.js';
|
|
8
|
+
import * as securityEngineer from './securityEngineer.js';
|
|
9
|
+
import * as softwareEngineer from './softwareEngineer.js';
|
|
10
|
+
import * as codeReviewer from './codeReviewer.js';
|
|
11
|
+
import * as sreEngineer from './sreEngineer.js';
|
|
12
|
+
import * as qaEngineer from './qaEngineer.js';
|
|
13
|
+
import * as technicalWriter from './technicalWriter.js';
|
|
14
|
+
import * as engineeringManager from './engineeringManager.js';
|
|
15
|
+
import * as mediaProducer from './mediaProducer.js';
|
|
16
|
+
import { checkCommandExecution, checkFileWrite, checkOutputWrite, READ_ONLY_ROLES, FORBIDDEN_COMMAND_PATTERNS } from './authority.js';
|
|
17
|
+
|
|
18
|
+
export const ROLES = {
|
|
19
|
+
'product-manager': productManager,
|
|
20
|
+
'system-analyst': systemAnalyst,
|
|
21
|
+
'software-architect': softwareArchitect,
|
|
22
|
+
'tech-lead': techLead,
|
|
23
|
+
'security-engineer': securityEngineer,
|
|
24
|
+
'software-engineer': softwareEngineer,
|
|
25
|
+
'code-reviewer': codeReviewer,
|
|
26
|
+
'sre-engineer': sreEngineer,
|
|
27
|
+
'qa-engineer': qaEngineer,
|
|
28
|
+
'technical-writer': technicalWriter,
|
|
29
|
+
'engineering-manager': engineeringManager,
|
|
30
|
+
'media-producer': mediaProducer,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Legacy name alias mapping for backwards compatibility
|
|
34
|
+
export const LEGACY_ALIASES = {
|
|
35
|
+
mission_compiler: 'product-manager',
|
|
36
|
+
cartographer: 'system-analyst',
|
|
37
|
+
chief_architect: 'software-architect',
|
|
38
|
+
architecture_adjudicator: 'tech-lead',
|
|
39
|
+
spec_critic: 'security-engineer',
|
|
40
|
+
implementer: 'software-engineer',
|
|
41
|
+
reviewer: 'code-reviewer',
|
|
42
|
+
failure_diagnostician: 'sre-engineer',
|
|
43
|
+
critical_acceptance: 'qa-engineer',
|
|
44
|
+
documentation: 'technical-writer',
|
|
45
|
+
next_mission_generator: 'engineering-manager',
|
|
46
|
+
studio_director: 'media-producer',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function getRole(idOrAlias) {
|
|
50
|
+
if (!idOrAlias) return null;
|
|
51
|
+
const normalized = idOrAlias.toLowerCase().replace(/_/g, '-');
|
|
52
|
+
if (ROLES[normalized]) return ROLES[normalized];
|
|
53
|
+
const mapped = LEGACY_ALIASES[idOrAlias.toLowerCase()];
|
|
54
|
+
if (mapped && ROLES[mapped]) return ROLES[mapped];
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function listRoles() {
|
|
59
|
+
return Object.values(ROLES).map((mod) => mod.definition);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export {
|
|
63
|
+
checkCommandExecution,
|
|
64
|
+
checkFileWrite,
|
|
65
|
+
checkOutputWrite,
|
|
66
|
+
READ_ONLY_ROLES,
|
|
67
|
+
FORBIDDEN_COMMAND_PATTERNS,
|
|
68
|
+
productManager,
|
|
69
|
+
systemAnalyst,
|
|
70
|
+
softwareArchitect,
|
|
71
|
+
techLead,
|
|
72
|
+
securityEngineer,
|
|
73
|
+
softwareEngineer,
|
|
74
|
+
codeReviewer,
|
|
75
|
+
sreEngineer,
|
|
76
|
+
qaEngineer,
|
|
77
|
+
technicalWriter,
|
|
78
|
+
engineeringManager,
|
|
79
|
+
mediaProducer,
|
|
80
|
+
};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Media Producer / Creative Director Role
|
|
2
|
+
// Portable specification and validation engine for local-first short-form video pipelines.
|
|
3
|
+
|
|
4
|
+
import { getCaptionTemplate } from '../captionTemplates.js';
|
|
5
|
+
|
|
6
|
+
export const definition = {
|
|
7
|
+
id: 'media-producer',
|
|
8
|
+
title: 'Media Producer',
|
|
9
|
+
formerName: 'studio_director',
|
|
10
|
+
category: 'media',
|
|
11
|
+
description: 'Authors and validates short-form video edit plans, timeline cuts, audio gain parameters, NFC text normalization, and media QA contracts.',
|
|
12
|
+
requiredCapabilities: ['chat'],
|
|
13
|
+
readOnly: false,
|
|
14
|
+
allowedOutputPrefixes: ['media/', 'reports/', 'packages/'],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function normalizeNfc(text = '') {
|
|
18
|
+
return typeof text === 'string' ? text.normalize('NFC') : '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validateCutInterval(interval) {
|
|
22
|
+
if (!interval || typeof interval !== 'object') throw new Error('CutInterval must be an object');
|
|
23
|
+
const { in_ms, out_ms } = interval;
|
|
24
|
+
if (!Number.isInteger(in_ms) || !Number.isInteger(out_ms)) {
|
|
25
|
+
throw new Error(`Interval bounds must be integers: [${in_ms}, ${out_ms})`);
|
|
26
|
+
}
|
|
27
|
+
if (in_ms < 0) throw new Error(`in_ms cannot be negative: ${in_ms}`);
|
|
28
|
+
if (out_ms <= in_ms) throw new Error(`Invalid interval: in_ms (${in_ms}) >= out_ms (${out_ms})`);
|
|
29
|
+
if (out_ms - in_ms < 100) throw new Error(`Interval duration too short (< 100ms): ${out_ms - in_ms}ms`);
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function validateVideoClip(clip) {
|
|
34
|
+
if (!clip || typeof clip !== 'object') throw new Error('VideoClip must be an object');
|
|
35
|
+
if (clip.kind !== 'video') throw new Error('VideoClip.kind must be "video"');
|
|
36
|
+
if (clip.speed !== undefined && clip.speed !== 1) throw new Error('V1 speed must be 1');
|
|
37
|
+
if (clip.timeline_start_ms < 0) throw new Error('timeline_start_ms cannot be negative');
|
|
38
|
+
validateCutInterval({ in_ms: clip.in_ms, out_ms: clip.out_ms });
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function validateTextClip(clip) {
|
|
43
|
+
if (!clip || typeof clip !== 'object') throw new Error('TextClip must be an object');
|
|
44
|
+
if (clip.kind !== 'text') throw new Error('TextClip.kind must be "text"');
|
|
45
|
+
const norm = normalizeNfc(clip.text);
|
|
46
|
+
if (norm.length < 1 || norm.length > 200) {
|
|
47
|
+
throw new Error(`TextClip text length must be between 1 and 200 NFC characters, got ${norm.length}`);
|
|
48
|
+
}
|
|
49
|
+
if (norm.includes('<') || norm.includes('>')) {
|
|
50
|
+
throw new Error('TextClip cannot contain HTML markup');
|
|
51
|
+
}
|
|
52
|
+
if (!Number.isInteger(clip.duration_ms) || clip.duration_ms < 100 || clip.duration_ms > 600000) {
|
|
53
|
+
throw new Error(`TextClip duration_ms must be between 100 and 600000ms, got ${clip.duration_ms}`);
|
|
54
|
+
}
|
|
55
|
+
if (clip.timeline_start_ms < 0) throw new Error('TextClip timeline_start_ms cannot be negative');
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function validateAudioTrack(track) {
|
|
60
|
+
if (!track || typeof track !== 'object') throw new Error('AudioTrack must be an object');
|
|
61
|
+
if (track.kind !== 'source') throw new Error('AudioTrack.kind must be "source"');
|
|
62
|
+
if (typeof track.gain_db !== 'number' || track.gain_db < -60 || track.gain_db > 12) {
|
|
63
|
+
throw new Error(`gain_db must be between -60 and +12 dB, got ${track.gain_db}`);
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function validateEditPlan(plan) {
|
|
69
|
+
if (!plan || typeof plan !== 'object') throw new Error('EditPlan must be an object');
|
|
70
|
+
if (plan.schema_version !== 1) throw new Error(`Unsupported schema_version: ${plan.schema_version}`);
|
|
71
|
+
if (!['edit_only', 'dark', 'podcast_clip'].includes(plan.mode)) {
|
|
72
|
+
throw new Error(`Unsupported mode: ${plan.mode}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (plan.caption_template) getCaptionTemplate(plan.caption_template);
|
|
76
|
+
if (Array.isArray(plan.captions)) {
|
|
77
|
+
for (const caption of plan.captions) {
|
|
78
|
+
if (caption.template) getCaptionTemplate(caption.template);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Clips validation
|
|
83
|
+
if (plan.mode === 'edit_only') {
|
|
84
|
+
if (!Array.isArray(plan.clips) || plan.clips.length !== 2) {
|
|
85
|
+
throw new Error(`V1 edit_only slice requires exactly 2 clips, got ${plan.clips?.length}`);
|
|
86
|
+
}
|
|
87
|
+
let timelinePos = 0;
|
|
88
|
+
for (let i = 0; i < plan.clips.length; i++) {
|
|
89
|
+
const c = plan.clips[i];
|
|
90
|
+
validateVideoClip(c);
|
|
91
|
+
if (c.timeline_start_ms !== timelinePos) {
|
|
92
|
+
throw new Error(`Clips must be contiguous: clip ${i} starts at ${c.timeline_start_ms}, expected ${timelinePos}`);
|
|
93
|
+
}
|
|
94
|
+
timelinePos += c.out_ms - c.in_ms;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!Array.isArray(plan.audio_tracks) || plan.audio_tracks.length !== plan.clips.length) {
|
|
98
|
+
throw new Error('Each video clip must have an accompanying source audio track');
|
|
99
|
+
}
|
|
100
|
+
for (let i = 0; i < plan.audio_tracks.length; i++) {
|
|
101
|
+
const a = plan.audio_tracks[i];
|
|
102
|
+
validateAudioTrack(a);
|
|
103
|
+
if (a.clip_id !== plan.clips[i].clip_id) {
|
|
104
|
+
throw new Error(`Audio track ${i} clip_id mismatch: expected ${plan.clips[i].clip_id}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} else if (plan.mode === 'dark') {
|
|
108
|
+
if (!Array.isArray(plan.clips) || plan.clips.length === 0) {
|
|
109
|
+
throw new Error('Dark mode requires at least one TextClip');
|
|
110
|
+
}
|
|
111
|
+
for (const c of plan.clips) {
|
|
112
|
+
validateTextClip(c);
|
|
113
|
+
}
|
|
114
|
+
} else if (plan.mode === 'podcast_clip') {
|
|
115
|
+
if (!Array.isArray(plan.clips) || plan.clips.length === 0) {
|
|
116
|
+
throw new Error('podcast_clip mode requires at least 1 video clip');
|
|
117
|
+
}
|
|
118
|
+
let timelinePos = 0;
|
|
119
|
+
for (let i = 0; i < plan.clips.length; i++) {
|
|
120
|
+
const c = plan.clips[i];
|
|
121
|
+
validateVideoClip(c);
|
|
122
|
+
if (c.timeline_start_ms !== timelinePos) {
|
|
123
|
+
throw new Error(`Clips must be contiguous: clip ${i} starts at ${c.timeline_start_ms}, expected ${timelinePos}`);
|
|
124
|
+
}
|
|
125
|
+
timelinePos += c.out_ms - c.in_ms;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!Array.isArray(plan.audio_tracks) || plan.audio_tracks.length !== plan.clips.length) {
|
|
129
|
+
throw new Error('Each video clip must have an accompanying source audio track');
|
|
130
|
+
}
|
|
131
|
+
for (let i = 0; i < plan.audio_tracks.length; i++) {
|
|
132
|
+
const a = plan.audio_tracks[i];
|
|
133
|
+
validateAudioTrack(a);
|
|
134
|
+
if (a.clip_id !== plan.clips[i].clip_id) {
|
|
135
|
+
throw new Error(`Audio track ${i} clip_id mismatch: expected ${plan.clips[i].clip_id}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (plan.captions) {
|
|
140
|
+
if (!Array.isArray(plan.captions)) {
|
|
141
|
+
throw new Error('captions must be an array');
|
|
142
|
+
}
|
|
143
|
+
for (const cap of plan.captions) {
|
|
144
|
+
const norm = normalizeNfc(cap.text);
|
|
145
|
+
if (norm.length < 1 || norm.length > 200) {
|
|
146
|
+
throw new Error(`Caption text length must be between 1 and 200 NFC characters, got ${norm.length}`);
|
|
147
|
+
}
|
|
148
|
+
if (norm.includes('<') || norm.includes('>')) {
|
|
149
|
+
throw new Error('Caption cannot contain HTML markup');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// B-roll overlays validation
|
|
156
|
+
if (plan.overlays) {
|
|
157
|
+
if (!Array.isArray(plan.overlays)) {
|
|
158
|
+
throw new Error('overlays must be an array');
|
|
159
|
+
}
|
|
160
|
+
for (const ov of plan.overlays) {
|
|
161
|
+
if (!ov || typeof ov !== 'object') throw new Error('Overlay must be an object');
|
|
162
|
+
if (!ov.asset_id && !ov.source_asset) throw new Error('Overlay requires asset_id');
|
|
163
|
+
if (typeof ov.in_ms !== 'number' || ov.in_ms < 0) throw new Error('Overlay in_ms must be >= 0');
|
|
164
|
+
if (typeof ov.out_ms !== 'number' || ov.out_ms <= ov.in_ms) throw new Error('Overlay out_ms must be > in_ms');
|
|
165
|
+
if (typeof ov.timeline_start_ms !== 'number' || ov.timeline_start_ms < 0) {
|
|
166
|
+
throw new Error('Overlay timeline_start_ms must be >= 0');
|
|
167
|
+
}
|
|
168
|
+
if (ov.layout && !['fullscreen', 'pip', 'split_bottom', 'split_top'].includes(ov.layout)) {
|
|
169
|
+
throw new Error(`Unsupported overlay layout: ${ov.layout}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Background music tracks validation
|
|
175
|
+
if (plan.music_tracks) {
|
|
176
|
+
if (!Array.isArray(plan.music_tracks)) {
|
|
177
|
+
throw new Error('music_tracks must be an array');
|
|
178
|
+
}
|
|
179
|
+
for (const m of plan.music_tracks) {
|
|
180
|
+
if (!m || typeof m !== 'object') throw new Error('music_track must be an object');
|
|
181
|
+
if (!m.asset_id && !m.source_asset) throw new Error('music_track requires asset_id');
|
|
182
|
+
if (m.gain_db !== undefined && (typeof m.gain_db !== 'number' || m.gain_db < -60 || m.gain_db > 12)) {
|
|
183
|
+
throw new Error('music_track gain_db must be between -60 and +12');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Output specification
|
|
189
|
+
const output = plan.output || {};
|
|
190
|
+
const validDimensions =
|
|
191
|
+
(output.width === 1080 && output.height === 1920) ||
|
|
192
|
+
(output.width === 1920 && output.height === 1080);
|
|
193
|
+
if (!validDimensions) {
|
|
194
|
+
throw new Error(`Output dimensions must be 1080x1920 (vertical) or 1920x1080 (horizontal), got ${output.width}x${output.height}`);
|
|
195
|
+
}
|
|
196
|
+
if (output.fps_num !== 30 || output.fps_den !== 1) {
|
|
197
|
+
throw new Error('Output FPS must be 30/1');
|
|
198
|
+
}
|
|
199
|
+
if (output.video_codec !== 'h264' || output.audio_codec !== 'aac') {
|
|
200
|
+
throw new Error('Output codecs must be h264 and aac');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export const validateContract = validateEditPlan;
|
|
207
|
+
|
|
208
|
+
export function buildPrompt(params = {}) {
|
|
209
|
+
const { mode = 'edit_only', assets = [] } = params;
|
|
210
|
+
return `You are acting as a Media Producer and Video Pipeline Director.
|
|
211
|
+
Generate a valid EditPlan contract for short-form video creation.
|
|
212
|
+
|
|
213
|
+
Mode: ${mode}
|
|
214
|
+
Available Assets:
|
|
215
|
+
${assets.map((a) => `- ${a}`).join('\n') || '- None provided'}
|
|
216
|
+
|
|
217
|
+
Rules:
|
|
218
|
+
1. Mode edit_only requires exactly 2 contiguous video clips from source media, each >= 100ms.
|
|
219
|
+
2. Mode dark requires TextClips with 1-200 NFC characters and zero HTML.
|
|
220
|
+
3. Mode podcast_clip requires >= 1 contiguous video clip(s) with accompanying source audio track(s) and optional captions.
|
|
221
|
+
4. Audio gain must stay between -60 dB and +12 dB.
|
|
222
|
+
5. Output must be 1080x1920 vertical @ 30fps with h264/aac encoding.`;
|
|
223
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Product Manager Role (formerly mission_compiler)
|
|
2
|
+
// Synthesizes business & human intent into validated requirement contracts and bounded scope.
|
|
3
|
+
|
|
4
|
+
export const definition = {
|
|
5
|
+
id: 'product-manager',
|
|
6
|
+
title: 'Product Manager',
|
|
7
|
+
formerName: 'mission_compiler',
|
|
8
|
+
category: 'product',
|
|
9
|
+
description: 'Compiles initial product briefs, synthesizes user intent into formal requirements, constraints, and scope boundaries.',
|
|
10
|
+
requiredCapabilities: ['chat'],
|
|
11
|
+
readOnly: false,
|
|
12
|
+
allowedOutputPrefixes: ['docs/', 'reports/', 'requirements/'],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function buildPrompt(params = {}) {
|
|
16
|
+
const { intent = '', constraints = [], scopeHint = [] } = params;
|
|
17
|
+
return `You are acting as a Staff Product Manager.
|
|
18
|
+
Analyze the human intent and produce a crisp, canonical requirements contract.
|
|
19
|
+
|
|
20
|
+
Input Intent:
|
|
21
|
+
${intent}
|
|
22
|
+
|
|
23
|
+
Constraints:
|
|
24
|
+
${constraints.map((c) => `- ${c}`).join('\n') || '- None provided'}
|
|
25
|
+
|
|
26
|
+
Scope Hint:
|
|
27
|
+
${scopeHint.map((s) => `- ${s}`).join('\n') || '- None provided'}
|
|
28
|
+
|
|
29
|
+
Output Requirements:
|
|
30
|
+
Return a structured JSON object with the following fields:
|
|
31
|
+
{
|
|
32
|
+
"objective": "A single concise statement of the primary goal",
|
|
33
|
+
"problem_statement": "Detailed description of the issue or feature",
|
|
34
|
+
"scope": ["concrete in-scope item 1", "concrete in-scope item 2"],
|
|
35
|
+
"out_of_scope": ["explicitly excluded items"],
|
|
36
|
+
"constraints": ["technical, operational, or business boundaries"],
|
|
37
|
+
"success_criteria": ["measurable criterion 1", "measurable criterion 2"],
|
|
38
|
+
"risk_level": "LOW | MEDIUM | HIGH | CRITICAL"
|
|
39
|
+
}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function validateContract(data) {
|
|
43
|
+
if (!data || typeof data !== 'object') throw new Error('Contract must be an object');
|
|
44
|
+
if (!data.objective || typeof data.objective !== 'string') throw new Error('Missing or invalid "objective"');
|
|
45
|
+
if (!Array.isArray(data.scope)) throw new Error('"scope" must be an array');
|
|
46
|
+
if (!Array.isArray(data.success_criteria) || data.success_criteria.length === 0) {
|
|
47
|
+
throw new Error('"success_criteria" must be a non-empty array');
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Quality Assurance Engineer / Release Gate Lead (formerly critical_acceptance)
|
|
2
|
+
// Verifies terminal operational criteria, test results, and issues definitive acceptance verdicts.
|
|
3
|
+
|
|
4
|
+
export const definition = {
|
|
5
|
+
id: 'qa-engineer',
|
|
6
|
+
title: 'QA Engineer',
|
|
7
|
+
formerName: 'critical_acceptance',
|
|
8
|
+
category: 'quality',
|
|
9
|
+
description: 'Validates operational criteria, executes end-to-end acceptance gates, and certifies releases.',
|
|
10
|
+
requiredCapabilities: ['chat'],
|
|
11
|
+
readOnly: true,
|
|
12
|
+
allowedOutputPrefixes: ['reports/'],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function buildPrompt(params = {}) {
|
|
16
|
+
const { criteria = [], testResults = {}, reviewVerdict = '' } = params;
|
|
17
|
+
return `You are acting as a Quality Assurance Lead and Release Acceptance Engineer.
|
|
18
|
+
Perform the final acceptance evaluation against operational success criteria.
|
|
19
|
+
|
|
20
|
+
Acceptance Criteria:
|
|
21
|
+
${criteria.map((c) => `- ${c}`).join('\n')}
|
|
22
|
+
|
|
23
|
+
Review Verdict:
|
|
24
|
+
${reviewVerdict}
|
|
25
|
+
|
|
26
|
+
Test Results:
|
|
27
|
+
${JSON.stringify(testResults, null, 2)}
|
|
28
|
+
|
|
29
|
+
Output Requirements:
|
|
30
|
+
Return a structured JSON object with:
|
|
31
|
+
{
|
|
32
|
+
"verdict": "APPROVED | REJECTED",
|
|
33
|
+
"criteria_results": [
|
|
34
|
+
{
|
|
35
|
+
"criterion": "Name of criterion",
|
|
36
|
+
"satisfied": true,
|
|
37
|
+
"evidence": "Observed test output or check"
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"open_risks": ["unresolved risks, if any"],
|
|
41
|
+
"release_readiness": "READY_FOR_PRODUCTION | BLOCKED"
|
|
42
|
+
}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function validateContract(data) {
|
|
46
|
+
if (!data || typeof data !== 'object') throw new Error('Contract must be an object');
|
|
47
|
+
if (!['APPROVED', 'REJECTED'].includes(data.verdict)) {
|
|
48
|
+
throw new Error('verdict must be APPROVED or REJECTED');
|
|
49
|
+
}
|
|
50
|
+
if (!Array.isArray(data.criteria_results)) {
|
|
51
|
+
throw new Error('"criteria_results" must be an array');
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|